Add base shell test system

- NOTE: very much wip and unstable
- This commit is a first step to provide boot style
  @ShellTest annotation
- New modules spring-shell-test and spring-shell-test-autoconfigure
- Focus is to autoconfigure context without shell runners so that
  we can create "sessions" and hook to configures jline terminal
  with custom in/out streams.
- Skeleton fork from jediterm to provide basic terminal emulation
  to part of a control amd escape characters working.
- ShellTestClient is a concept user can use to interact with a shell
  in a same way user would use a "real" shell.
- Fixes #489
This commit is contained in:
Janne Valkealahti
2022-11-30 09:09:35 +00:00
parent 2bfcf99e96
commit d3e897f6d9
82 changed files with 9694 additions and 0 deletions

View File

@@ -49,6 +49,8 @@ include 'spring-shell-samples'
include 'spring-shell-standard'
include 'spring-shell-standard-commands'
include 'spring-shell-table'
include 'spring-shell-test'
include 'spring-shell-test-autoconfigure'
file("${rootDir}/spring-shell-starters").eachDirMatch(~/spring-shell-starter.*/) {
include "spring-shell-starters:${it.name}"

View File

@@ -7,7 +7,9 @@ description = 'Spring Shell Documentation'
dependencies {
management platform(project(":spring-shell-management"))
implementation project(':spring-shell-starters:spring-shell-starter')
implementation project(':spring-shell-starters:spring-shell-starter-test')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.awaitility:awaitility'
}
asciidoctorj {

View File

@@ -0,0 +1,25 @@
[[using-shell-testing-basics]]
==== Basics
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
Spring Shell provides a number of utilities and annotations to help when testing your application.
Test support is provided by two modules: `spring-shell-test` contains core items, and
`spring-shell-test-autoconfigure` supports auto-configuration for tests.
To test _interactive_ commands.
====
[source, java, indent=0]
----
include::{snippets}/TestingSnippets.java[tag=testing-shelltest-interactive]
----
====
To test _non-interactive_ commands.
====
[source, java, indent=0]
----
include::{snippets}/TestingSnippets.java[tag=testing-shelltest-noninteractive]
----
====

View File

@@ -0,0 +1,18 @@
[[using-shell-testing]]
== Testing
ifndef::snippets[:snippets: ../../test/java/org/springframework/shell/docs]
Testing cli application is difficult due to various reasons:
- There are differences between OS's.
- Within OS there may be different shell implementations in use.
- What goes into a shell and comes out from a shell my be totally
different what you see in shell itself due to control characters.
- Shell may feel syncronous but most likely it is not meaning when
someting is written into it, you can't assume next update in
in it is not final.
NOTE: Testing support is currently under development and will be
unstable for various parts.
include::using-shell-testing-basics.adoc[]

View File

@@ -13,3 +13,5 @@ include::using-shell-components.adoc[]
include::using-shell-customization.adoc[]
include::using-shell-execution.adoc[]
include::using-shell-testing.adoc[]

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2022 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.docs;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.test.ShellAssertions;
import org.springframework.shell.test.ShellTestClient;
import org.springframework.shell.test.ShellTestClient.InteractiveShellSession;
import org.springframework.shell.test.ShellTestClient.NonInteractiveShellSession;
import org.springframework.shell.test.autoconfigure.ShellTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import static org.awaitility.Awaitility.await;
class TestingSnippets {
// tag::testing-shelltest-interactive[]
@ShellTest
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
class InteractiveTestSample {
@Autowired
ShellTestClient client;
@Test
void test() {
InteractiveShellSession session = client
.interactive()
.run();
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen())
.containsText("shell");
});
session.write(session.writeSequence().text("help").carriageReturn().build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen())
.containsText("AVAILABLE COMMANDS");
});
}
}
// end::testing-shelltest-interactive[]
// tag::testing-shelltest-noninteractive[]
@ShellTest
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
class NonInteractiveTestSample {
@Autowired
ShellTestClient client;
@Test
void test() {
NonInteractiveShellSession session = client
.nonInterative("help", "help")
.run();
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen())
.containsText("AVAILABLE COMMANDS");
});
}
}
// end::testing-shelltest-noninteractive[]
}

View File

@@ -9,7 +9,9 @@ description = 'Spring Shell Samples'
dependencies {
management platform(project(":spring-shell-management"))
implementation project(':spring-shell-starters:spring-shell-starter-jna')
testImplementation project(':spring-shell-starters:spring-shell-starter-test')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.awaitility:awaitility'
}
springBoot {

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2022 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;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.shell.samples.standard.ResolvedCommands;
import org.springframework.shell.test.ShellAssertions;
import org.springframework.shell.test.ShellTestClient;
import org.springframework.shell.test.ShellTestClient.BaseShellSession;
import org.springframework.shell.test.ShellTestClient.InteractiveShellSession;
import org.springframework.shell.test.ShellTestClient.NonInteractiveShellSession;
import org.springframework.shell.test.autoconfigure.ShellTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import static org.awaitility.Awaitility.await;
@ShellTest
@Import(ResolvedCommands.ResolvedCommandsConfiguration.class)
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class AbstractSampleTests {
@Autowired
protected ShellTestClient client;
protected void assertScreenContainsText(BaseShellSession<?> session, String text) {
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen()).containsText(text);
});
}
protected BaseShellSession<?> createSession(String command, boolean interactive) {
if (interactive) {
InteractiveShellSession session = client.interactive().run();
session.write(session.writeSequence().command(command).build());
return session;
}
else {
String[] commands = command.split(" ");
NonInteractiveShellSession session = client.nonInterative(commands).run();
return session;
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2022 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.e2e;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.shell.samples.AbstractSampleTests;
import org.springframework.shell.test.ShellTestClient.BaseShellSession;
class RequiredValueCommandsTests extends AbstractSampleTests {
@ParameterizedTest
@CsvSource({
"e2e anno required-value,false",
"e2e reg required-value,false",
"e2e anno required-value,true",
"e2e reg required-value,true"
})
void shouldRequireOption(String command, boolean interactive) {
BaseShellSession<?> session = createSession(command, interactive);
assertScreenContainsText(session, "Missing mandatory option");
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2022 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.standard;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.shell.samples.AbstractSampleTests;
import org.springframework.shell.test.ShellAssertions;
import org.springframework.shell.test.ShellTestClient.BaseShellSession;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
public class ComponentCommandsTests extends AbstractSampleTests {
@ParameterizedTest
@CsvSource({
"component single,false",
"component single,true"
})
void componentSingle(String command, boolean interactive) {
BaseShellSession<?> session = createSession(command, interactive);
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.screen().lines()).anySatisfy(line -> {
assertThat(line).containsPattern("[>] key1");
});
});
session.write(session.writeSequence().keyDown().build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.screen().lines()).anySatisfy(line -> {
assertThat(line).containsPattern("[>] key2");
});
});
session.write(session.writeSequence().cr().build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen()).containsText("Got value value2");
});
}
@ParameterizedTest
@CsvSource({
"component multi,false",
"component multi,true"
})
void componentMulti(String command, boolean interactive) {
BaseShellSession<?> session = createSession(command, interactive);
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.screen().lines()).anySatisfy(line -> {
assertThat(line).containsPattern("[>] (☐|\\[ \\]) key1");
});
});
session.write(session.writeSequence().space().build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.screen().lines()).anySatisfy(line -> {
assertThat(line).containsPattern("[>] (☒|\\[x\\]) key1");
});
});
session.write(session.writeSequence().cr().build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen()).containsText("Got value value1,value2");
});
}
}

View File

@@ -0,0 +1,12 @@
plugins {
id 'org.springframework.shell.starter'
}
description = 'Spring Shell Starter Test'
dependencies {
management platform(project(":spring-shell-management"))
api(project(':spring-shell-starters:spring-shell-starter'))
api(project(":spring-shell-test"))
api(project(":spring-shell-test-autoconfigure"))
}

View File

@@ -0,0 +1,21 @@
plugins {
id 'org.springframework.shell.module'
}
description = 'Spring Shell Test Autoconfigure'
dependencies {
management platform(project(":spring-shell-management"))
implementation project(':spring-shell-core')
implementation project(':spring-shell-standard')
implementation project(':spring-shell-test')
implementation project(':spring-shell-autoconfigure')
implementation 'org.springframework:spring-test'
implementation 'org.springframework.boot:spring-boot-autoconfigure'
implementation 'org.springframework.boot:spring-boot-test-autoconfigure'
implementation 'org.springframework.boot:spring-boot-starter-test'
optional 'org.jline:jline'
optional 'org.assertj:assertj-core'
optional 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.awaitility:awaitility'
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2022 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.test.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
/**
* {@link ImportAutoConfiguration Auto-configuration imports} for typical Shell tests.
* Most tests should consider using {@link ShellTest @ShellTest} rather than using this
* annotation directly.
*
* @author Janne Valkealahti
* @see ShellTest
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
public @interface AutoConfigureShell {
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2022 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.test.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
/**
* Annotation that can be applied to a test class to enable and configure
* auto-configuration of shell client.
*
* @author Janne Valkealahti
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
public @interface AutoConfigureShellTestClient {
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2022 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.test.autoconfigure;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.boot.JLineShellAutoConfiguration;
import org.springframework.shell.boot.TerminalCustomizer;
import org.springframework.shell.test.jediterm.terminal.TtyConnector;
import org.springframework.shell.test.jediterm.terminal.ui.JediTermWidget;
import org.springframework.shell.test.jediterm.terminal.ui.TerminalSession;
@AutoConfiguration(before = JLineShellAutoConfiguration.class)
public class ShellAutoConfiguration {
@Bean
TerminalCustomizer terminalStreamsTerminalCustomizer(TerminalStreams terminalStreams) {
return builder -> {
builder.streams(terminalStreams.input, terminalStreams.output)
.jansi(false)
.jna(false);
};
}
@Bean
TerminalStreams terminalStreams() {
return new TerminalStreams();
}
@Bean
TtyConnector ttyConnector(TerminalStreams terminalStreams) {
return new TestTtyConnector(terminalStreams.myReader, terminalStreams.myWriter);
}
@Bean
TerminalSession terminalSession(TtyConnector ttyConnector) {
JediTermWidget widget = new JediTermWidget(80, 24);
widget.setTtyConnector(ttyConnector);
return widget;
}
public static class TerminalStreams {
PipedInputStream input;
PipedOutputStream output;
InputStreamReader myReader;
OutputStreamWriter myWriter;
public TerminalStreams() {
input = new PipedInputStream();
output = new PipedOutputStream();
try {
myReader = new InputStreamReader(new PipedInputStream(this.output));
myWriter = new OutputStreamWriter(new PipedOutputStream(this.input));
} catch (IOException e) {
}
}
}
private static class TestTtyConnector implements TtyConnector {
private final static Logger log = LoggerFactory.getLogger(TestTtyConnector.class);
InputStreamReader myReader;
OutputStreamWriter myWriter;
TestTtyConnector(InputStreamReader myReader, OutputStreamWriter myWriter) {
this.myReader = myReader;
this.myWriter = myWriter;
}
@Override
public boolean init() {
return true;
}
@Override
public void close() {
}
@Override
public String getName() {
return null;
}
@Override
public int read(char[] buf, int offset, int length) throws IOException {
log.trace("read1");
int read = this.myReader.read(buf, offset, length);
log.trace("read2 {}", read);
return read;
}
@Override
public void write(byte[] bytes) throws IOException {
log.trace("write1 {}", bytes);
this.myWriter.write(new String(bytes));
this.myWriter.flush();
log.trace("write2");
}
@Override
public boolean isConnected() {
return true;
}
@Override
public void write(String string) throws IOException {
this.write(string.getBytes());
}
@Override
public int waitFor() throws InterruptedException {
return 0;
}
@Override
public boolean ready() throws IOException {
log.trace("ready1");
boolean ready = myReader.ready();
log.trace("ready2 {}", ready);
return ready;
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2022 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.test.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration;
import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.env.Environment;
import org.springframework.test.context.BootstrapWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* Annotation that can be used for a Shell test that focuses
* <strong>only</strong> on Shell components.
*
* @author Janne Valkealahti
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(ShellTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(ShellTypeExcludeFilter.class)
@AutoConfigureShell
@AutoConfigureShellTestClient
@ImportAutoConfiguration
public @interface ShellTest {
/**
* Properties in form {@literal key=value} that should be added to the Spring
* {@link Environment} before the test runs.
*
* @return the properties to add
*/
String[] properties() default {};
/**
* Determines if default filtering should be used with
* {@link SpringBootApplication @SpringBootApplication}.
*
* @see #includeFilters()
* @see #excludeFilters()
* @return if default filters should be used
*/
boolean useDefaultFilters() default true;
/**
* A set of include filters which can be used to add otherwise filtered beans to the
* application context.
*
* @return include filters to apply
*/
Filter[] includeFilters() default {};
/**
* A set of exclude filters which can be used to filter beans that would otherwise be
* added to the application context.
*
* @return exclude filters to apply
*/
Filter[] excludeFilters() default {};
/**
* Auto-configuration exclusions that should be applied for this test.
*
* @return auto-configuration exclusions to apply
*/
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
Class<?>[] excludeAutoConfiguration() default {};
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2022 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.test.autoconfigure;
import org.jline.reader.LineReader;
import org.jline.terminal.Terminal;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.shell.Shell;
import org.springframework.shell.jline.PromptProvider;
import org.springframework.shell.test.ShellTestClient;
import org.springframework.shell.test.jediterm.terminal.ui.TerminalSession;
/**
*
* @author Janne Valkealahti
*/
@AutoConfiguration
public class ShellTestClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ShellTestClient shellTestClient(TerminalSession widget, Shell shell, PromptProvider promptProvider,
LineReader lineReader, Terminal terminal) {
return ShellTestClient.builder(widget, shell, promptProvider, lineReader, terminal).build();
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2022 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.test.autoconfigure;
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.test.context.TestContextBootstrapper;
/**
* {@link TestContextBootstrapper} for {@link ShellTest @ShellTest} support.
*
* @author Janne Valkealahti
*/
public class ShellTestContextBootstrapper extends SpringBootTestContextBootstrapper {
@Override
protected String[] getProperties(Class<?> testClass) {
return MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS).get(ShellTest.class)
.getValue("properties", String[].class).orElse(null);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2022 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.test.autoconfigure;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.boot.test.autoconfigure.filter.StandardAnnotationCustomizableTypeExcludeFilter;
import org.springframework.shell.standard.ShellComponent;
/**
* {@link TypeExcludeFilter} for {@link ShellTest @ShellTest}.
*
* @author Janne Valkealahti
*/
public class ShellTypeExcludeFilter extends StandardAnnotationCustomizableTypeExcludeFilter<ShellTest> {
private static final Set<Class<?>> DEFAULT_INCLUDES;
static {
Set<Class<?>> includes = new LinkedHashSet<>();
includes.add(ShellComponent.class);
DEFAULT_INCLUDES = Collections.unmodifiableSet(includes);
}
ShellTypeExcludeFilter(Class<?> testClass) {
super(testClass);
}
@Override
protected Set<Class<?>> getDefaultIncludes() {
return DEFAULT_INCLUDES;
}
}

View File

@@ -0,0 +1,16 @@
org.springframework.shell.test.autoconfigure.ShellAutoConfiguration
org.springframework.shell.test.autoconfigure.ShellTestClientAutoConfiguration
org.springframework.shell.boot.CommandCatalogAutoConfiguration
org.springframework.shell.boot.CompleterAutoConfiguration
org.springframework.shell.boot.ComponentFlowAutoConfiguration
org.springframework.shell.boot.ExitCodeAutoConfiguration
org.springframework.shell.boot.JLineAutoConfiguration
org.springframework.shell.boot.JLineShellAutoConfiguration
org.springframework.shell.boot.LineReaderAutoConfiguration
org.springframework.shell.boot.ParameterResolverAutoConfiguration
org.springframework.shell.boot.ShellContextAutoConfiguration
org.springframework.shell.boot.SpringShellAutoConfiguration
org.springframework.shell.boot.StandardAPIAutoConfiguration
org.springframework.shell.boot.StandardCommandsAutoConfiguration
org.springframework.shell.boot.ThemingAutoConfiguration
org.springframework.shell.boot.UserConfigAutoConfiguration

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2022 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.test.autoconfigure;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell.test.ShellAssertions;
import org.springframework.shell.test.ShellTestClient;
import org.springframework.shell.test.ShellTestClient.InteractiveShellSession;
import org.springframework.shell.test.ShellTestClient.NonInteractiveShellSession;
import org.springframework.shell.test.autoconfigure.app.ExampleShellApplication;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
@ContextConfiguration(classes = ExampleShellApplication.class)
@ShellTest
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public class ShellTestIntegrationTests {
@Autowired
ShellTestClient client;
@Test
void testInteractive1() throws Exception {
InteractiveShellSession session = client.interactive().run();
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen()).containsText("shell");
});
session.write(session.writeSequence().text("help").carriageReturn().build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen()).containsText("AVAILABLE COMMANDS");
});
session.write(session.writeSequence().carriageReturn().build());
await().atMost(4, TimeUnit.SECONDS).untilAsserted(() -> {
List<String> lines = session.screen().lines();
Condition<String> prompt = new Condition<>(line -> line.contains("shell:"), "Shell has expected prompt");
assertThat(lines).areExactly(3, prompt);
});
session.write(session.writeSequence().ctrl('l').build());
await().atMost(4, TimeUnit.SECONDS).untilAsserted(() -> {
List<String> lines = session.screen().lines();
Condition<String> prompt = new Condition<>(line -> line.contains("shell:"), "Shell has expected prompt");
assertThat(lines).areExactly(1, prompt);
});
session.write(session.writeSequence().ctrl('c').build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.isComplete()).isTrue();
});
}
@Test
void testInteractive2() throws Exception {
InteractiveShellSession session = client.interactive().run();
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
ShellAssertions.assertThat(session.screen()).containsText("shell:");
});
session.write(session.writeSequence().ctrl('c').build());
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.isComplete()).isTrue();
});
}
@Test
void testNonInteractive() throws Exception {
Condition<String> helpCondition = new Condition<>(line -> line.contains("AVAILABLE COMMANDS"),
"Help has expected output");
Condition<String> helpHelpCondition = new Condition<>(line -> line.contains("help - Display help about available commands"),
"Help help has expected output");
NonInteractiveShellSession session = client.nonInterative("help").run();
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
List<String> lines = session.screen().lines();
assertThat(lines).areExactly(1, helpCondition);
assertThat(lines).areNot(helpHelpCondition);
});
session.write(session.writeSequence().clearScreen().build());
NonInteractiveShellSession session2 = client.nonInterative("help", "help").run();
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
List<String> lines = session2.screen().lines();
assertThat(lines).areNot(helpCondition);
assertThat(lines).areExactly(1, helpHelpCondition);
});
session.write(session.writeSequence().ctrl('c').build());
await().atMost(2, TimeUnit.SECONDS).untilAsserted(() -> {
assertThat(session.isComplete()).isTrue();
});
}
@Test
void testNonInteractive2() throws Exception {
Condition<String> helloCondition = new Condition<>(line -> line.contains("hello"),
"Hello has expected output");
NonInteractiveShellSession session = client.nonInterative("hello").run();
await().atMost(30, TimeUnit.SECONDS).untilAsserted(() -> {
List<String> lines = session.screen().lines();
assertThat(lines).areExactly(1, helloCondition);
});
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2022 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.test.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.shell.test.autoconfigure.app.ExampleShellApplication;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for the {@link ShellTest#properties properties} attribute of
* {@link ShellTest @ShellTest}.
*
* @author Janne Valkealahti
*/
@ShellTest(properties = "spring.profiles.active=test")
@ContextConfiguration(classes = ExampleShellApplication.class)
public class ShellTestPropertiesIntegrationTests {
@Autowired
private Environment environment;
@Test
void environmentWithNewProfile() {
assertThat(this.environment.getActiveProfiles()).containsExactly("test");
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2022 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.test.autoconfigure.app;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.shell.test.autoconfigure.ShellTest;
/**
* Example {@link SpringBootApplication @SpringBootApplication} for use with
* {@link ShellTest @ShellTest} tests.
*
* @author Janne Valkealahti
*/
@SpringBootApplication
public class ExampleShellApplication {
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright 2022 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.test.autoconfigure.app;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
@ShellComponent
public class HelloCommand {
@ShellMethod
public String hello() {
return "hello";
}
}

View File

@@ -0,0 +1,9 @@
#logging:
# file:
# name: xxx.log
# level:
# root: debug
# org:
# jline: debug
# springframework:
# shell: trace

View File

@@ -0,0 +1,14 @@
plugins {
id 'org.springframework.shell.module'
}
description = 'Spring Shell Test'
dependencies {
management platform(project(":spring-shell-management"))
implementation project(':spring-shell-core')
optional 'org.assertj:assertj-core'
optional 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.awaitility:awaitility'
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2022 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.test;
import org.assertj.core.api.InstanceOfAssertFactory;
/**
* Entry point for assertion methods for shell components.
*
* @author Janne Valkealahti
*/
public class ShellAssertions {
/**
* Instance of a assert factory for {@link ShellScreen}.
*/
public static final InstanceOfAssertFactory<ShellScreen, ShellScreenAssert> SHELLSCREEN = new InstanceOfAssertFactory<>(ShellScreen.class,
ShellAssertions::assertThat);
/**
* Creates an instance of {@link ShellScreenAssert}.
*
* @param actual the actual value
* @return the created assertion object
*/
public static ShellScreenAssert assertThat(ShellScreen actual) {
return new ShellScreenAssert(actual);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2022 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.test;
import java.util.List;
/**
* Interface representing a shell screen.
*
* @author Janne Valkealahti
*/
public interface ShellScreen {
/**
* Gets a visible lines in a screen.
*
* @return visible lines in a screen
*/
List<String> lines();
/**
* Get {@code ShellScreen} out of lines.
*
* @param lines the lines
* @return instance of shell screen
*/
static ShellScreen of(List<String> lines) {
return new DefaultShellScreen(lines);
}
class DefaultShellScreen implements ShellScreen {
List<String> lines;
DefaultShellScreen(List<String> lines) {
this.lines = lines;
}
@Override
public List<String> lines() {
return lines;
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2022 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.test;
import java.util.List;
import java.util.stream.Collectors;
import org.assertj.core.api.AbstractAssert;
/**
* Asserts for {@link ShellScreen}.
*
* @author Janne Valkealahti
*/
public class ShellScreenAssert extends AbstractAssert<ShellScreenAssert, ShellScreen> {
public ShellScreenAssert(ShellScreen actual) {
super(actual, ShellScreenAssert.class);
}
/**
* Verifies that text if found from a screen.
*
* @param text the text to look for
* @return this assertion object
*/
public ShellScreenAssert containsText(String text) {
isNotNull();
List<String> lines = actual.lines();
boolean match = lines.stream().filter(n -> n.contains(text)).findFirst().isPresent();
if (!match) {
failWithMessage("Expected to find %s from screen but was %s", text,
lines.stream().collect(Collectors.joining("\n")));
}
return this;
}
}

View File

@@ -0,0 +1,357 @@
/*
* Copyright 2022 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.test;
import java.io.Closeable;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicInteger;
import org.jline.reader.LineReader;
import org.jline.terminal.Terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.DefaultApplicationArguments;
import org.springframework.shell.Shell;
import org.springframework.shell.ShellRunner;
import org.springframework.shell.context.DefaultShellContext;
import org.springframework.shell.jline.InteractiveShellRunner;
import org.springframework.shell.jline.NonInteractiveShellRunner;
import org.springframework.shell.jline.PromptProvider;
import org.springframework.shell.test.jediterm.terminal.ui.TerminalSession;
/**
* Client for terminal session which can be used as a programmatic way
* to interact with a shell application. In a typical test it is required
* to write into a shell and read what is visible in a shell.
*
* @author Janne Valkealahti
*/
public interface ShellTestClient extends Closeable {
/**
* Run interactive shell session.
*
* @return session for chaining
*/
InteractiveShellSession interactive();
/**
* Run non-interactive command session.
*
* @param args the command arguments
* @return session for chaining
*/
NonInteractiveShellSession nonInterative(String... args);
/**
* Read the screen.
*
* @return the screen
*/
ShellScreen screen();
/**
* Get an instance of a builder.
*
* @param terminalSession the terminal session
* @param shell the shell
* @param promptProvider the prompt provider
* @param lineReader the line reader
* @param terminal the terminal
* @return a Builder
*/
public static Builder builder(TerminalSession terminalSession, Shell shell, PromptProvider promptProvider,
LineReader lineReader, Terminal terminal) {
return new DefaultBuilder(terminalSession, shell, promptProvider, lineReader, terminal);
}
/**
* Builder interface for {@code ShellClient}.
*/
interface Builder {
/**
* Build a shell client.
*
* @return a shell client
*/
ShellTestClient build();
}
interface BaseShellSession<T extends BaseShellSession<T>> {
/**
* Get a write sequencer.
*
* @return a write sequencer
*/
ShellWriteSequence writeSequence();
/**
* Read the screen.
*
* @return the screen
*/
ShellScreen screen();
/**
* Write plain text into a shell.
*
* @param text the text
* @return client for chaining
*/
T write(String text);
/**
* Run a session.
*
* @return client for chaining
*/
T run();
boolean isComplete();
}
interface InteractiveShellSession extends BaseShellSession<InteractiveShellSession> {
}
interface NonInteractiveShellSession extends BaseShellSession<NonInteractiveShellSession> {
}
static class DefaultBuilder implements Builder {
private TerminalSession terminalSession;
private Shell shell;
private PromptProvider promptProvider;
private LineReader lineReader;
private Terminal terminal;
DefaultBuilder(TerminalSession terminalSession, Shell shell, PromptProvider promptProvider,
LineReader lineReader, Terminal terminal) {
this.terminalSession = terminalSession;
this.shell = shell;
this.promptProvider = promptProvider;
this.lineReader = lineReader;
this.terminal = terminal;
}
@Override
public ShellTestClient build() {
return new DefaultShellClient(terminalSession, shell, promptProvider, lineReader, terminal);
}
}
static class DefaultShellClient implements ShellTestClient {
private final static Logger log = LoggerFactory.getLogger(DefaultShellClient.class);
private TerminalSession terminalSession;
private Shell shell;
private PromptProvider promptProvider;
private LineReader lineReader;
private Thread runnerThread;
private Terminal terminal;
private final BlockingQueue<ShellRunnerTaskData> blockingQueue = new LinkedBlockingDeque<>(10);
DefaultShellClient(TerminalSession terminalSession, Shell shell, PromptProvider promptProvider,
LineReader lineReader, Terminal terminal) {
this.terminalSession = terminalSession;
this.shell = shell;
this.promptProvider = promptProvider;
this.lineReader = lineReader;
this.terminal = terminal;
}
@Override
public InteractiveShellSession interactive() {
terminalSession.start();
if (runnerThread == null) {
runnerThread = new Thread(new ShellRunnerTask(this.blockingQueue));
runnerThread.start();
}
return new DefaultInteractiveShellSession(shell, promptProvider, lineReader, blockingQueue, terminalSession, terminal);
}
@Override
public NonInteractiveShellSession nonInterative(String... args) {
terminalSession.start();
if (runnerThread == null) {
runnerThread = new Thread(new ShellRunnerTask(this.blockingQueue));
runnerThread.start();
}
return new DefaultNonInteractiveShellSession(shell, args, blockingQueue, terminalSession, terminal);
}
@Override
public ShellScreen screen() {
return ShellScreen.of(terminalSession.getTerminalTextBuffer().getScreen());
}
@Override
public void close() throws IOException {
log.debug("Closing ShellClient");
if (runnerThread != null) {
runnerThread.interrupt();
}
runnerThread = null;
terminalSession.close();
}
}
static class DefaultInteractiveShellSession implements InteractiveShellSession {
private Shell shell;
private PromptProvider promptProvider;
private LineReader lineReader;
private BlockingQueue<ShellRunnerTaskData> blockingQueue;
private TerminalSession terminalSession;
private Terminal terminal;
private final AtomicInteger state = new AtomicInteger(-2);
public DefaultInteractiveShellSession(Shell shell, PromptProvider promptProvider, LineReader lineReader,
BlockingQueue<ShellRunnerTaskData> blockingQueue, TerminalSession terminalSession, Terminal terminal) {
this.shell = shell;
this.promptProvider = promptProvider;
this.lineReader = lineReader;
this.blockingQueue = blockingQueue;
this.terminalSession = terminalSession;
this.terminal = terminal;
}
@Override
public ShellWriteSequence writeSequence() {
return ShellWriteSequence.of(terminal);
}
@Override
public InteractiveShellSession write(String text) {
terminalSession.getTerminalStarter().sendString(text);
return this;
}
@Override
public ShellScreen screen() {
return ShellScreen.of(terminalSession.getTerminalTextBuffer().getScreen());
}
@Override
public InteractiveShellSession run() {
ShellRunner runner = new InteractiveShellRunner(lineReader, promptProvider, shell, new DefaultShellContext());
ApplicationArguments appArgs = new DefaultApplicationArguments();
this.blockingQueue.add(new ShellRunnerTaskData(runner, appArgs, state));
return this;
}
@Override
public boolean isComplete() {
return state.get() >= 0;
}
}
static class DefaultNonInteractiveShellSession implements NonInteractiveShellSession {
private Shell shell;
private String[] args;
private BlockingQueue<ShellRunnerTaskData> blockingQueue;
private TerminalSession terminalSession;
private Terminal terminal;
private final AtomicInteger state = new AtomicInteger(-2);
public DefaultNonInteractiveShellSession(Shell shell, String[] args,
BlockingQueue<ShellRunnerTaskData> blockingQueue, TerminalSession terminalSession, Terminal terminal) {
this.shell = shell;
this.args = args;
this.blockingQueue = blockingQueue;
this.terminalSession = terminalSession;
this.terminal = terminal;
}
@Override
public ShellWriteSequence writeSequence() {
return ShellWriteSequence.of(terminal);
}
@Override
public NonInteractiveShellSession write(String text) {
terminalSession.getTerminalStarter().sendString(text);
return this;
}
@Override
public ShellScreen screen() {
return ShellScreen.of(terminalSession.getTerminalTextBuffer().getScreen());
}
@Override
public NonInteractiveShellSession run() {
ShellRunner runner = new NonInteractiveShellRunner(shell, new DefaultShellContext());
ApplicationArguments appArgs = new DefaultApplicationArguments(args);
this.blockingQueue.add(new ShellRunnerTaskData(runner, appArgs, state));
return this;
}
@Override
public boolean isComplete() {
return state.get() >= 0;
}
}
static record ShellRunnerTaskData(
ShellRunner runner,
ApplicationArguments args,
AtomicInteger state
) {}
static class ShellRunnerTask implements Runnable {
private final static Logger log = LoggerFactory.getLogger(ShellRunnerTask.class);
private BlockingQueue<ShellRunnerTaskData> blockingQueue;
ShellRunnerTask(BlockingQueue<ShellRunnerTaskData> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
log.trace("ShellRunnerTask start");
try {
Thread.currentThread().setName("ShellRunnerTask");
while (true) {
ShellRunnerTaskData data = blockingQueue.take();
if (data.runner == null) {
return;
}
try {
log.trace("Running {}", data.runner());
data.state().set(-1);
data.runner().run(data.args());
data.state().set(0);
log.trace("Running done {}", data.runner());
} catch (Exception e) {
data.state().set(1);
log.trace("ShellRunnerThread ex", e);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.trace("ShellRunnerTask end");
}
}
}

View File

@@ -0,0 +1,206 @@
/*
* Copyright 2022 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.test;
import org.jline.keymap.KeyMap;
import org.jline.terminal.Terminal;
import org.jline.utils.InfoCmp;
/**
* Interface sequencing various things into terminal aware text types.
*
* @author Janne Valkealahti
*/
public interface ShellWriteSequence {
/**
* Sequence terminal clear screen.
*
* @return a sequence for chaining
*/
ShellWriteSequence clearScreen();
/**
* Sequence terminal carriage return.
*
* @return a sequence for chaining
*/
ShellWriteSequence carriageReturn();
/**
* Sequence from command with expected {@code carriage return}.
*
* @param command the command
* @return a sequence for chaining
*/
ShellWriteSequence command(String command);
/**
* Sequence terminal carriage return. Alias for {@link #carriageReturn}
*
* @return a sequence for chaining
* @see #carriageReturn()
*/
ShellWriteSequence cr();
/**
* Sequence text.
*
* @param text the text
* @return a sequence for chaining
*/
ShellWriteSequence text(String text);
/**
* Sequence terminal key down.
*
* @return a sequence for chaining
*/
ShellWriteSequence keyDown();
/**
* Sequence terminal key left.
*
* @return a sequence for chaining
*/
ShellWriteSequence keyLeft();
/**
* Sequence terminal key right.
*
* @return a sequence for chaining
*/
ShellWriteSequence keyRight();
/**
* Sequence terminal key up.
*
* @return a sequence for chaining
*/
ShellWriteSequence keyUp();
/**
* Sequence terminal space.
*
* @return a sequence for chaining
*/
ShellWriteSequence space();
/**
* Sequence terminal ctrl.
*
* @return a sequence for chaining
*/
ShellWriteSequence ctrl(char c);
/**
* Build the result.
*
* @return the result
*/
String build();
/**
* Get a new instance of a {@code ShellWriteSequence}.
*
* @param terminal the terminal
* @return instance of a write sequence
*/
static ShellWriteSequence of(Terminal terminal) {
return new DefaultShellWriteSequence(terminal);
}
static class DefaultShellWriteSequence implements ShellWriteSequence {
private final Terminal terminal;
private StringBuilder buf = new StringBuilder();
DefaultShellWriteSequence(Terminal terminal) {
this.terminal = terminal;
}
@Override
public ShellWriteSequence carriageReturn() {
this.buf.append(KeyMap.key(this.terminal, InfoCmp.Capability.carriage_return));
return this;
}
@Override
public ShellWriteSequence clearScreen() {
String ansiClearScreen = KeyMap.key(this.terminal, InfoCmp.Capability.clear_screen);
this.buf.append(ansiClearScreen);
return this;
}
@Override
public ShellWriteSequence ctrl(char c) {
String ctrl = KeyMap.ctrl(c);
this.buf.append(ctrl);
return this;
}
@Override
public ShellWriteSequence command(String command) {
this.text(command);
return carriageReturn();
}
@Override
public ShellWriteSequence cr() {
return carriageReturn();
}
@Override
public ShellWriteSequence keyUp() {
this.buf.append(KeyMap.key(this.terminal, InfoCmp.Capability.key_up));
return this;
}
@Override
public ShellWriteSequence keyDown() {
this.buf.append(KeyMap.key(this.terminal, InfoCmp.Capability.key_down));
return this;
}
@Override
public ShellWriteSequence keyLeft() {
this.buf.append(KeyMap.key(this.terminal, InfoCmp.Capability.key_left));
return this;
}
@Override
public ShellWriteSequence keyRight() {
this.buf.append(KeyMap.key(this.terminal, InfoCmp.Capability.key_right));
return this;
}
@Override
public ShellWriteSequence text(String text) {
this.buf.append(text);
return this;
}
@Override
public ShellWriteSequence space() {
return this.text(" ");
}
@Override
public String build() {
return buf.toString();
}
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import java.io.IOException;
import org.springframework.shell.test.jediterm.terminal.util.CharUtils;
/**
* Takes data from underlying char array.
*
* @author jediterm authors
*/
public class ArrayTerminalDataStream implements TerminalDataStream {
protected char[] buf;
protected int offset;
protected int length;
public ArrayTerminalDataStream(char[] buf, int offset, int length) {
this.buf = buf;
this.offset = offset;
this.length = length;
}
public ArrayTerminalDataStream(char[] buf) {
this(buf, 0, buf.length);
}
@Override
public char getChar() throws IOException {
if (this.length == 0) {
throw new EOF();
}
this.length--;
return this.buf[this.offset++];
}
@Override
public void pushChar(final char c) throws EOF {
if (this.offset == 0) {
// Pushed back too many... shift it up to the end.
char[] newBuf;
if (this.buf.length - this.length == 0) {
newBuf = new char[this.buf.length + 1];
}
else {
newBuf = this.buf;
}
this.offset = newBuf.length - this.length;
System.arraycopy(this.buf, 0, newBuf, this.offset, this.length);
this.buf = newBuf;
}
this.length++;
this.buf[--this.offset] = c;
}
@Override
public String readNonControlCharacters(int maxChars) throws IOException {
String nonControlCharacters = CharUtils.getNonControlCharacters(maxChars, this.buf, this.offset, this.length);
this.offset += nonControlCharacters.length();
this.length -= nonControlCharacters.length();
return nonControlCharacters;
}
@Override
public void pushBackBuffer(final char[] bytes, final int length) throws EOF {
for (int i = length - 1; i >= 0; i--) {
pushChar(bytes[i]);
}
}
@Override
public boolean isEmpty() {
return this.length == 0;
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
/**
* Current cursor shape as described by https://vt100.net/docs/vt510-rm/DECSCUSR.html.
*
* @author jediterm authors
*/
public enum CursorShape {
BLINK_BLOCK,
STEADY_BLOCK,
BLINK_UNDERLINE,
STEADY_UNDERLINE,
BLINK_VERTICAL_BAR,
STEADY_VERTICAL_BAR
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import java.io.IOException;
import org.springframework.shell.test.jediterm.terminal.emulator.Emulator;
/**
* @author jediterm authors
*/
public abstract class DataStreamIteratingEmulator implements Emulator {
protected final TerminalDataStream myDataStream;
protected final Terminal myTerminal;
private boolean myEof = false;
public DataStreamIteratingEmulator(TerminalDataStream dataStream, Terminal terminal) {
myDataStream = dataStream;
myTerminal = terminal;
}
@Override
public boolean hasNext() {
return !myEof;
}
@Override
public void resetEof() {
myEof = false;
}
@Override
public void next() throws IOException {
try {
char b = myDataStream.getChar();
processChar(b, myTerminal);
}
catch (TerminalDataStream.EOF e) {
myEof = true;
}
}
protected abstract void processChar(char ch, Terminal terminal) throws IOException;
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
/**
*
* @author jediterm authors
*/
public enum RequestOrigin{
User,
Remote
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import org.springframework.shell.test.jediterm.terminal.model.CharBuffer;
/**
* General interface that obtains styled range of characters at coordinates (<b>x</b>, <b>y</b>) when the screen starts at <b>startRow</b>
*
* @author jediterm authors
*/
public interface StyledTextConsumer {
/**
*
* @param x indicates starting column of the characters
* @param y indicates row of the characters
* @param style style of characters
* @param characters text characters
* @param startRow number of the first row.
* It can be different for different buffers, e.g. backBuffer starts from 0, textBuffer and scrollBuffer from -count
*/
void consume(int x, int y, TextStyle style, CharBuffer characters, int startRow);
void consumeNul(int x, int y, int nulIndex, TextStyle style, CharBuffer characters, int startRow);
void consumeQueue(int x, int y, int nulIndex, int startRow);
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import org.springframework.shell.test.jediterm.terminal.model.CharBuffer;
/**
*
* @author jediterm authors
*/
public class StyledTextConsumerAdapter implements StyledTextConsumer {
public void consume(int x, int y, TextStyle style, CharBuffer characters, int startRow) {
// to override
}
@Override
public void consumeNul(int x, int y, int nulIndex, TextStyle style, CharBuffer characters, int startRow) {
// to override
}
@Override
public void consumeQueue(int x, int y, int nulIndex, int startRow) {
// to override
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.CompletableFuture;
import org.springframework.shell.test.jediterm.terminal.model.StyleState;
/**
* Executes terminal commands interpreted by {@link org.springframework.shell.test.jediterm.terminal.emulator.Emulator}, receives text
*
* @author jediterm authors
*/
public interface Terminal {
void resize(int width, int height, RequestOrigin origin);
void resize(int width, int height, RequestOrigin origin, CompletableFuture<?> promptUpdated);
void beep();
void backspace();
void horizontalTab();
void carriageReturn();
void newLine();
void mapCharsetToGL(int num);
void mapCharsetToGR(int num);
void designateCharacterSet(int tableNumber, char ch);
void setAnsiConformanceLevel(int level);
void writeDoubleByte(char[] bytes) throws UnsupportedEncodingException;
void writeCharacters(String string);
int distanceToLineEnd();
void reverseIndex();
void index();
void nextLine();
void fillScreen(char c);
void saveCursor();
void restoreCursor();
void reset();
void characterAttributes(TextStyle textStyle);
void setScrollingRegion(int top, int bottom);
void scrollUp(int count);
void scrollDown(int count);
void resetScrollRegions();
void cursorHorizontalAbsolute(int x);
void linePositionAbsolute(int y);
void cursorPosition(int x, int y);
void cursorUp(int countY);
void cursorDown(int dY);
void cursorForward(int dX);
void cursorBackward(int dX);
void cursorShape(CursorShape shape);
void eraseInLine(int arg);
void deleteCharacters(int count);
int getTerminalWidth();
int getTerminalHeight();
void eraseInDisplay(int arg);
void setModeEnabled(TerminalMode mode, boolean enabled);
void disconnected();
int getCursorX();
int getCursorY();
void singleShiftSelect(int num);
void setWindowTitle(String name);
void saveWindowTitleOnStack();
void restoreWindowTitleFromStack();
void clearScreen();
void setCursorVisible(boolean visible);
void useAlternateBuffer(boolean enabled);
// byte[] getCodeForKey(int key, int modifiers);
void setApplicationArrowKeys(boolean enabled);
void setApplicationKeypad(boolean enabled);
void setAutoNewLine(boolean enabled);
StyleState getStyleState();
void insertLines(int count);
void deleteLines(int count);
void setBlinkingCursor(boolean enabled);
void eraseCharacters(int count);
void insertBlankCharacters(int count);
void clearTabStopAtCursor();
void clearAllTabStops();
void setTabStopAtCursor();
void writeUnwrappedString(String string);
void setTerminalOutput(TerminalOutputStream terminalOutput);
void setAltSendsEscape(boolean enabled);
void deviceStatusReport(String str);
void deviceAttributes(byte[] response);
void setBracketedPasteMode(boolean enabled);
// TerminalColor getWindowForeground();
// TerminalColor getWindowBackground();
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
/**
*
* @author jediterm authors
*/
public interface TerminalCopyPasteHandler {
void setContents(String text, boolean useSystemSelectionClipboardIfAvailable);
String getContents(boolean useSystemSelectionClipboardIfAvailable);
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import java.io.IOException;
/**
* Represents data communication interface for terminal.
* It allows to {@link #getChar()} by one and {@link #pushChar(char)} back as well as requesting a chunk of plain ASCII
* characters ({@link #readNonControlCharacters(int)} - for faster processing from buffer in the size {@literal <=maxChars}).
*
*
* @author jediterm authors
*/
public interface TerminalDataStream {
char getChar() throws IOException;
void pushChar(char c) throws IOException;
String readNonControlCharacters(int maxChars) throws IOException;
void pushBackBuffer(char[] bytes, int length) throws IOException;
boolean isEmpty();
class EOF extends IOException {
public EOF() {
super("EOF: There is no more data or connection is lost");
}
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
/**
*
* @author jediterm authors
*/
public interface TerminalDisplay {
int getRowCount();
int getColumnCount();
void beep();
void scrollArea(final int scrollRegionTop, final int scrollRegionSize, int dy);
String getWindowTitle();
void setWindowTitle(String name);
boolean ambiguousCharsAreDoubleWidth();
default void setBracketedPasteMode(boolean enabled) {}
// default TerminalColor getWindowForeground() {
// return null;
// }
// default TerminalColor getWindowBackground() {
// return null;
// }
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author jediterm authors
*/
public enum TerminalMode {
Null,
CursorKey {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
terminal.setApplicationArrowKeys(enabled);
}
},
ANSI,
WideColumn {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
// Skip resizing as it would require to resize parent container.
// Other terminal emulators (iTerm2, Terminal.app, GNOME Terminal) ignore it too.
terminal.clearScreen();
terminal.resetScrollRegions();
}
},
CursorVisible {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
terminal.setCursorVisible(enabled);
}
},
AlternateBuffer {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
terminal.useAlternateBuffer(enabled);
}
},
SmoothScroll,
ReverseVideo,
OriginMode {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
}
},
AutoWrap {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
//we do nothing just switching the mode
}
},
AutoRepeatKeys,
Interlace,
Keypad {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
terminal.setApplicationKeypad(enabled);
}
},
StoreCursor {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
if (enabled) {
terminal.saveCursor();
}
else {
terminal.restoreCursor();
}
}
},
CursorBlinking {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
terminal.setBlinkingCursor(enabled);
}
},
AllowWideColumn,
ReverseWrapAround,
AutoNewLine {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
terminal.setAutoNewLine(enabled);
}
},
KeyboardAction,
InsertMode,
SendReceive,
EightBitInput, //Interpret "meta" key, sets eighth bit. (enables the eightBitInput resource).
// http://www.leonerd.org.uk/hacks/hints/xterm-8bit.html
AltSendsEscape //See section Alt and Meta Keys in http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
{
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
terminal.setAltSendsEscape(enabled);
}
},
// https://cirw.in/blog/bracketed-paste
// http://www.xfree86.org/current/ctlseqs.html#Bracketed%20Paste%20Mode
BracketedPasteMode {
@Override
public void setEnabled(Terminal terminal, boolean enabled) {
terminal.setBracketedPasteMode(enabled);
}
}
;
private static final Logger LOG = LoggerFactory.getLogger(TerminalMode.class);
public void setEnabled(Terminal terminal, boolean enabled) {
LOG.warn("Mode " + name() + " is not implemented, setting to " + enabled);
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
/**
* Sends a response from the terminal emulator.
*
* @author jediterm authors
*/
public interface TerminalOutputStream {
/**
* @deprecated use {@link #sendBytes(byte[], boolean)} instead
*/
@Deprecated
@SuppressWarnings("DeprecatedIsStillUsed")
void sendBytes(byte[] response);
/**
* @deprecated use {@link #sendString(String, boolean)} instead
*/
@Deprecated
@SuppressWarnings("DeprecatedIsStillUsed")
void sendString(final String string);
default void sendBytes(byte[] response, boolean userInput) {
sendBytes(response);
}
default void sendString(String string, boolean userInput) {
sendString(string);
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.shell.test.jediterm.terminal.emulator.Emulator;
import org.springframework.shell.test.jediterm.terminal.emulator.JediEmulator;
/**
* Runs terminal emulator. Manages threads to send response.
*
* @author jediterm authors
*/
public class TerminalStarter implements TerminalOutputStream {
private static final Logger LOG = LoggerFactory.getLogger(TerminalStarter.class);
private final Emulator myEmulator;
private final Terminal myTerminal;
private final TtyConnector myTtyConnector;
private final ScheduledExecutorService myEmulatorExecutor = Executors.newSingleThreadScheduledExecutor();
public TerminalStarter(final Terminal terminal, final TtyConnector ttyConnector, TerminalDataStream dataStream) {
myTtyConnector = ttyConnector;
myTerminal = terminal;
myTerminal.setTerminalOutput(this);
myEmulator = createEmulator(dataStream, terminal);
}
protected JediEmulator createEmulator(TerminalDataStream dataStream, Terminal terminal) {
return new JediEmulator(dataStream, terminal);
}
private void execute(Runnable runnable) {
if (!myEmulatorExecutor.isShutdown()) {
myEmulatorExecutor.execute(runnable);
}
}
public void start() {
try {
while (!Thread.currentThread().isInterrupted() && myEmulator.hasNext()) {
myEmulator.next();
}
}
catch (final InterruptedIOException e) {
LOG.info("Terminal exiting");
}
catch (final Exception e) {
if (!myTtyConnector.isConnected()) {
myTerminal.disconnected();
return;
}
LOG.error("Caught exception in terminal thread", e);
}
}
// public byte[] getCode(final int key, final int modifiers) {
// return myTerminal.getCodeForKey(key, modifiers);
// }
public void postResize(int width, int height, RequestOrigin origin) {
execute(() -> {
resize(myEmulator, myTerminal, myTtyConnector, width, height, origin, (millisDelay, runnable) -> {
myEmulatorExecutor.schedule(runnable, millisDelay, TimeUnit.MILLISECONDS);
});
});
}
/**
* Resizes terminal and tty connector, should be called on a pooled thread.
*/
public static void resize(Emulator emulator, Terminal terminal, TtyConnector ttyConnector, int width, int height,
RequestOrigin origin, BiConsumer<Long, Runnable> taskScheduler) {
CompletableFuture<?> promptUpdated = ((JediEmulator)emulator).getPromptUpdatedAfterResizeFuture(taskScheduler);
terminal.resize(width, height, origin, promptUpdated);
ttyConnector.resize(width, height);
}
@Override
public void sendBytes(final byte[] bytes) {
sendBytes(bytes, false);
}
@Override
public void sendBytes(final byte[] bytes, boolean userInput) {
execute(() -> {
try {
myTtyConnector.write(bytes);
}
catch (IOException e) {
throw new RuntimeException(e);
}
});
}
@Override
public void sendString(final String string) {
sendString(string, false);
}
@Override
public void sendString(final String string, boolean userInput) {
execute(() -> {
try {
myTtyConnector.write(string);
}
catch (IOException e) {
throw new RuntimeException(e);
}
});
}
public void close() {
execute(() -> {
try {
myTtyConnector.close();
}
catch (Exception e) {
LOG.error("Error closing terminal", e);
}
finally {
myEmulatorExecutor.shutdown();
}
});
}
}

View File

@@ -0,0 +1,175 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import java.lang.ref.WeakReference;
import java.util.EnumSet;
import java.util.Objects;
import java.util.WeakHashMap;
/**
*
* @author jediterm authors
*/
public class TextStyle {
private static final EnumSet<Option> NO_OPTIONS = EnumSet.noneOf(Option.class);
public static final TextStyle EMPTY = new TextStyle();
private static final WeakHashMap<TextStyle, WeakReference<TextStyle>> styles = new WeakHashMap<>();
// private final TerminalColor myForeground;
// private final TerminalColor myBackground;
private final EnumSet<Option> myOptions;
public TextStyle() {
// this(null, null, NO_OPTIONS);
this(NO_OPTIONS);
}
// public TextStyle(TerminalColor foreground, TerminalColor background) {
// this(foreground, background, NO_OPTIONS);
// }
// public TextStyle(TerminalColor foreground, TerminalColor background, EnumSet<Option> options) {
// myForeground = foreground;
// myBackground = background;
// myOptions = options.clone();
// }
public TextStyle(EnumSet<Option> options) {
myOptions = options.clone();
}
public static TextStyle getCanonicalStyle(TextStyle currentStyle) {
final WeakReference<TextStyle> canonRef = styles.get(currentStyle);
if (canonRef != null) {
final TextStyle canonStyle = canonRef.get();
if (canonStyle != null) {
return canonStyle;
}
}
styles.put(currentStyle, new WeakReference<>(currentStyle));
return currentStyle;
}
// public TerminalColor getForeground() {
// return myForeground;
// }
// public TerminalColor getBackground() {
// return myBackground;
// }
public TextStyle createEmptyWithColors() {
// return new TextStyle(myForeground, myBackground);
return new TextStyle();
}
public int getId() {
return hashCode();
}
public boolean hasOption(final Option option) {
return myOptions.contains(option);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TextStyle textStyle = (TextStyle) o;
return
// Objects.equals(myForeground, textStyle.myForeground) &&
// Objects.equals(myBackground, textStyle.myBackground) &&
myOptions.equals(textStyle.myOptions);
}
@Override
public int hashCode() {
// return Objects.hash(myForeground, myBackground, myOptions);
return Objects.hash(myOptions);
}
// public TerminalColor getBackgroundForRun() {
// return myOptions.contains(Option.INVERSE) ? myForeground : myBackground;
// }
// public TerminalColor getForegroundForRun() {
// return myOptions.contains(Option.INVERSE) ? myBackground : myForeground;
// }
public Builder toBuilder() {
return new Builder(this);
}
public enum Option {
BOLD,
ITALIC,
BLINK,
DIM,
INVERSE,
UNDERLINED,
HIDDEN;
private void set(EnumSet<Option> options, boolean val) {
if (val) {
options.add(this);
}
else {
options.remove(this);
}
}
}
public static class Builder {
// private TerminalColor myForeground;
// private TerminalColor myBackground;
private EnumSet<Option> myOptions;
public Builder(TextStyle textStyle) {
// myForeground = textStyle.myForeground;
// myBackground = textStyle.myBackground;
myOptions = textStyle.myOptions.clone();
}
public Builder() {
// myForeground = null;
// myBackground = null;
myOptions = EnumSet.noneOf(Option.class);
}
// public Builder setForeground(TerminalColor foreground) {
// myForeground = foreground;
// return this;
// }
// public Builder setBackground(TerminalColor background) {
// myBackground = background;
// return this;
// }
public Builder setOption(Option option, boolean val) {
option.set(myOptions, val);
return this;
}
public TextStyle build() {
// return new TextStyle(myForeground, myBackground, myOptions);
return new TextStyle(myOptions);
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import java.io.IOException;
import org.springframework.shell.test.jediterm.terminal.util.CharUtils;
/**
* Takes data from and sends it back to TTY input and output streams via {@link TtyConnector}
*
* @author jediterm authors
*/
public class TtyBasedArrayDataStream extends ArrayTerminalDataStream {
private final TtyConnector ttyConnector;
private final Runnable myOnBeforeBlockingWait;
public TtyBasedArrayDataStream(final TtyConnector ttyConnector, final Runnable onBeforeBlockingWait) {
super(new char[1024], 0, 0);
this.ttyConnector = ttyConnector;
myOnBeforeBlockingWait = onBeforeBlockingWait;
}
public TtyBasedArrayDataStream(final TtyConnector ttyConnector) {
super(new char[1024], 0, 0);
this.ttyConnector = ttyConnector;
myOnBeforeBlockingWait = null;
}
@Override
public char getChar() throws IOException {
if (length == 0) {
fillBuf();
}
return super.getChar();
}
@Override
public String readNonControlCharacters(int maxChars) throws IOException {
if (length == 0) {
fillBuf();
}
return super.readNonControlCharacters(maxChars);
}
@Override
public String toString() {
return CharUtils.toHumanReadableText(new String(buf, offset, length));
}
private void fillBuf() throws IOException {
offset = 0;
if (!this.ttyConnector.ready() && myOnBeforeBlockingWait != null) {
myOnBeforeBlockingWait.run();
}
length = this.ttyConnector.read(buf, offset, buf.length);
if (length <= 0) {
length = 0;
throw new EOF();
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2022 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.test.jediterm.terminal;
import java.io.IOException;
/**
* Interface to tty.
*
* @author jediterm authors
*/
public interface TtyConnector {
boolean init();
void close();
default void resize(int width, int height) {
// support old implementations not overriding this method
resize(width, height);
// StackOverflowError is only possible if both resize(Dimension) and resize(Dimension,Dimension) are not overridden.
}
// /**
// * @deprecated use {@link #resize(Dimension)} instead
// */
// @SuppressWarnings("unused")
// @Deprecated
// default void resize(int width, int height, int pixelSizeWidth, int pixelSizeHeight) {
// // support old code that calls this method on new implementations (not overriding this deprecated method)
// resize(width, height);
// }
String getName();
int read(char[] buf, int offset, int length) throws IOException;
void write(byte[] bytes) throws IOException;
boolean isConnected();
void write(String string) throws IOException;
int waitFor() throws InterruptedException;
boolean ready() throws IOException;
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright 2022 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.test.jediterm.terminal.emulator;
import java.io.IOException;
import java.util.ArrayList;
import org.springframework.shell.test.jediterm.terminal.TerminalDataStream;
import org.springframework.shell.test.jediterm.terminal.util.CharUtils;
import org.springframework.shell.test.jediterm.typeahead.Ascii;
/**
* @author jediterm authors
*/
public class ControlSequence {
private int myArgc;
private int[] myArgv;
private char myFinalChar;
private ArrayList<Character> myUnhandledChars;
private boolean myStartsWithQuestionMark = false; // true when CSI ?
private boolean myStartsWithMoreMark = false; // true when CSI >
private final StringBuilder mySequenceString = new StringBuilder();
ControlSequence(final TerminalDataStream channel) throws IOException {
myArgv = new int[5];
myArgc = 0;
readControlSequence(channel);
}
private void readControlSequence(final TerminalDataStream channel) throws IOException {
myArgc = 0;
// Read integer arguments
int digit = 0;
int seenDigit = 0;
int pos = -1;
while (true) {
final char b = channel.getChar();
mySequenceString.append(b);
pos++;
if (b == '?' && pos == 0) {
myStartsWithQuestionMark = true;
}
else if (b == '>' && pos == 0) {
myStartsWithMoreMark = true;
}
else if (b == ';') {
if (digit > 0) {
myArgc++;
if (myArgc == myArgv.length) {
int[] replacement = new int[myArgv.length * 2];
System.arraycopy(myArgv, 0, replacement, 0, myArgv.length);
myArgv = replacement;
}
myArgv[myArgc] = 0;
digit = 0;
}
}
else if ('0' <= b && b <= '9') {
myArgv[myArgc] = myArgv[myArgc] * 10 + b - '0';
digit++;
seenDigit = 1;
}
else if (':' <= b && b <= '?') {
addUnhandled(b);
}
else if (0x40 <= b && b <= 0x7E) {
myFinalChar = b;
break;
}
else {
addUnhandled(b);
}
}
myArgc += seenDigit;
}
private void addUnhandled(final char b) {
if (myUnhandledChars == null) {
myUnhandledChars = new ArrayList<>();
}
myUnhandledChars.add(b);
}
public boolean pushBackReordered(final TerminalDataStream channel) throws IOException {
if (myUnhandledChars == null) return false;
final char[] bytes = new char[1024]; // can't be more than the whole buffer...
int i = 0;
for (final char b : myUnhandledChars) {
bytes[i++] = b;
}
bytes[i++] = (byte)Ascii.ESC;
bytes[i++] = (byte)'[';
if (myStartsWithQuestionMark) {
bytes[i++] = (byte)'?';
}
if (myStartsWithMoreMark) {
bytes[i++] = (byte)'>';
}
for (int argi = 0; argi < myArgc; argi++) {
if (argi != 0) bytes[i++] = ';';
String s = Integer.toString(myArgv[argi]);
for (int j = 0; j < s.length(); j++) {
bytes[i++] = s.charAt(j);
}
}
bytes[i++] = myFinalChar;
channel.pushBackBuffer(bytes, i);
return true;
}
int getCount() {
return myArgc;
}
final int getArg(final int index, final int defaultValue) {
if (index >= myArgc) {
return defaultValue;
}
return myArgv[index];
}
private void appendToBuffer(final StringBuilder sb) {
sb.append("ESC[");
if (myStartsWithQuestionMark) {
sb.append("?");
}
if (myStartsWithMoreMark) {
sb.append(">");
}
String sep = "";
for (int i = 0; i < myArgc; i++) {
sb.append(sep);
sb.append(myArgv[i]);
sep = ";";
}
sb.append(myFinalChar);
if (myUnhandledChars != null) {
sb.append(" Unhandled:");
CharUtils.CharacterType last = CharUtils.CharacterType.NONE;
for (final char b : myUnhandledChars) {
last = CharUtils.appendChar(sb, last, b);
}
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
appendToBuffer(sb);
return sb.toString();
}
public char getFinalChar() {
return myFinalChar;
}
public boolean startsWithQuestionMark() {
return myStartsWithQuestionMark;
}
public boolean startsWithMoreMark() {
return myStartsWithMoreMark;
}
public String getDebugInfo() {
StringBuilder sb = new StringBuilder();
sb.append("parsed: ");
appendToBuffer(sb);
sb.append(", raw: ESC[");
sb.append(mySequenceString);
return sb.toString();
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2022 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.test.jediterm.terminal.emulator;
import java.io.IOException;
/**
* @author jediterm authors
*/
public interface Emulator {
boolean hasNext();
void next() throws IOException;
void resetEof();
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2022 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.test.jediterm.terminal.emulator;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.shell.test.jediterm.terminal.TerminalDataStream;
import org.springframework.shell.test.jediterm.terminal.util.CharUtils;
import org.springframework.shell.test.jediterm.typeahead.Ascii;
/**
* @author jediterm authors
*/
final class SystemCommandSequence {
private static final char ST = 0x9c;
private final List<Object> myArgs = new ArrayList<>();
private final StringBuilder mySequence = new StringBuilder();
public SystemCommandSequence( TerminalDataStream stream) throws IOException {
StringBuilder argBuilder = new StringBuilder();
boolean end = false;
while (!end) {
char ch = stream.getChar();
mySequence.append(ch);
end = isEnd();
if (ch == ';' || end) {
if (end && isTwoBytesEnd()) {
argBuilder.deleteCharAt(argBuilder.length() - 1);
}
myArgs.add(parseArg(argBuilder.toString()));
argBuilder.setLength(0);
}
else {
argBuilder.append(ch);
}
}
}
private Object parseArg( String arg) {
if (arg.length() > 0 && Character.isDigit(arg.charAt(arg.length() - 1))) {
// check isDigit to reduce amount of expensive NumberFormatException
try {
return Integer.parseInt(arg);
}
catch (NumberFormatException ignored) {
}
}
return arg;
}
private boolean isEnd() {
int len = mySequence.length();
if (len > 0) {
char ch = mySequence.charAt(len - 1);
return ch == Ascii.BEL || ch == ST || isTwoBytesEnd();
}
return false;
}
private boolean isTwoBytesEnd() {
int len = mySequence.length();
return len > 1 && mySequence.charAt(len - 2) == Ascii.ESC && mySequence.charAt(len - 1) == '\\';
}
public String getStringAt(int i) {
if (i>=myArgs.size()) {
return null;
}
Object val = myArgs.get(i);
return val instanceof String ? (String)val : null;
}
public int getIntAt(int position, int defaultValue) {
if (position < myArgs.size()) {
Object val = myArgs.get(position);
if (val instanceof Integer) {
return (Integer) val;
}
}
return defaultValue;
}
public String format( String body) {
return (char)Ascii.ESC + "]" + body + getTerminator();
}
@Override
public String toString() {
return CharUtils.toHumanReadableText(mySequence.toString());
}
/**
* <a href="https://invisible-island.net/xterm/ctlseqs/ctlseqs.html">
* XTerm accepts either BEL or ST for terminating OSC
* sequences, and when returning information, uses the same
* terminator used in a query. </a>
*/
private String getTerminator() {
int len = mySequence.length();
if (isTwoBytesEnd()) {
return mySequence.substring(len - 2);
}
return mySequence.substring(len - 1);
}
}

View File

@@ -0,0 +1,439 @@
/*
* Copyright 2022 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.test.jediterm.terminal.emulator.charset;
/**
* Provides an enum with names for the supported character sets.
*
* @author jediterm authors
*/
public enum CharacterSet
{
ASCII( 'B' )
{
@Override
public int map( int index )
{
return -1;
}
},
BRITISH( 'A' )
{
@Override
public int map( int index )
{
if ( index == 3 )
{
// Pound sign...
return '\u00a3';
}
return -1;
}
},
DANISH( 'E', '6' )
{
@Override
public int map( int index )
{
switch ( index )
{
case 32:
return '\u00c4';
case 59:
return '\u00c6';
case 60:
return '\u00d8';
case 61:
return '\u00c5';
case 62:
return '\u00dc';
case 64:
return '\u00e4';
case 91:
return '\u00e6';
case 92:
return '\u00f8';
case 93:
return '\u00e5';
case 94:
return '\u00fc';
default:
return -1;
}
}
},
DEC_SPECIAL_GRAPHICS( '0', '2' )
{
@Override
public int map( int index )
{
if ( index >= 64 && index < 96 )
{
return ( ( Character )CharacterSets.DEC_SPECIAL_CHARS[index - 64][0] ).charValue();
}
return -1;
}
},
DEC_SUPPLEMENTAL( 'U', '<' )
{
@Override
public int map( int index )
{
if ( index >= 0 && index < 64 )
{
// Set the 8th bit...
return index + 160;
}
return -1;
}
},
DUTCH( '4' )
{
@Override
public int map( int index )
{
switch ( index )
{
case 3:
return '\u00a3';
case 32:
return '\u00be';
case 59:
return '\u0133';
case 60:
return '\u00bd';
case 61:
return '|';
case 91:
return '\u00a8';
case 92:
return '\u0192';
case 93:
return '\u00bc';
case 94:
return '\u00b4';
default:
return -1;
}
}
},
FINNISH( 'C', '5' )
{
@Override
public int map( int index )
{
switch ( index )
{
case 59:
return '\u00c4';
case 60:
return '\u00d4';
case 61:
return '\u00c5';
case 62:
return '\u00dc';
case 64:
return '\u00e9';
case 91:
return '\u00e4';
case 92:
return '\u00f6';
case 93:
return '\u00e5';
case 94:
return '\u00fc';
default:
return -1;
}
}
},
FRENCH( 'R' )
{
@Override
public int map( int index )
{
switch ( index )
{
case 3:
return '\u00a3';
case 32:
return '\u00e0';
case 59:
return '\u00b0';
case 60:
return '\u00e7';
case 61:
return '\u00a6';
case 91:
return '\u00e9';
case 92:
return '\u00f9';
case 93:
return '\u00e8';
case 94:
return '\u00a8';
default:
return -1;
}
}
},
FRENCH_CANADIAN( 'Q' )
{
@Override
public int map( int index )
{
switch ( index )
{
case 32:
return '\u00e0';
case 59:
return '\u00e2';
case 60:
return '\u00e7';
case 61:
return '\u00ea';
case 62:
return '\u00ee';
case 91:
return '\u00e9';
case 92:
return '\u00f9';
case 93:
return '\u00e8';
case 94:
return '\u00fb';
default:
return -1;
}
}
},
GERMAN( 'K' )
{
@Override
public int map( int index )
{
switch ( index )
{
case 32:
return '\u00a7';
case 59:
return '\u00c4';
case 60:
return '\u00d6';
case 61:
return '\u00dc';
case 91:
return '\u00e4';
case 92:
return '\u00f6';
case 93:
return '\u00fc';
case 94:
return '\u00df';
default:
return -1;
}
}
},
ITALIAN( 'Y' )
{
@Override
public int map( int index )
{
switch ( index )
{
case 3:
return '\u00a3';
case 32:
return '\u00a7';
case 59:
return '\u00ba';
case 60:
return '\u00e7';
case 61:
return '\u00e9';
case 91:
return '\u00e0';
case 92:
return '\u00f2';
case 93:
return '\u00e8';
case 94:
return '\u00ec';
default:
return -1;
}
}
},
SPANISH( 'Z' )
{
@Override
public int map( int index )
{
switch ( index )
{
case 3:
return '\u00a3';
case 32:
return '\u00a7';
case 59:
return '\u00a1';
case 60:
return '\u00d1';
case 61:
return '\u00bf';
case 91:
return '\u00b0';
case 92:
return '\u00f1';
case 93:
return '\u00e7';
default:
return -1;
}
}
},
SWEDISH( 'H', '7' )
{
@Override
public int map( int index )
{
switch ( index )
{
case 32:
return '\u00c9';
case 59:
return '\u00c4';
case 60:
return '\u00d6';
case 61:
return '\u00c5';
case 62:
return '\u00dc';
case 64:
return '\u00e9';
case 91:
return '\u00e4';
case 92:
return '\u00f6';
case 93:
return '\u00e5';
case 94:
return '\u00fc';
default:
return -1;
}
}
},
SWISS( '=' )
{
@Override
public int map( int index )
{
switch ( index )
{
case 3:
return '\u00f9';
case 32:
return '\u00e0';
case 59:
return '\u00e9';
case 60:
return '\u00e7';
case 61:
return '\u00ea';
case 62:
return '\u00ee';
case 63:
return '\u00e8';
case 64:
return '\u00f4';
case 91:
return '\u00e4';
case 92:
return '\u00f6';
case 93:
return '\u00fc';
case 94:
return '\u00fb';
default:
return -1;
}
}
};
private final int[] myDesignations;
/**
* Creates a new {@link CharacterSet} instance.
*
* @param designations the characters that designate this character set, cannot
* be {@code null}.
*/
CharacterSet(int... designations)
{
myDesignations = designations;
}
// METHODS
/**
* Returns the {@link CharacterSet} for the given character.
*
* @param designation the character to translate to a {@link CharacterSet}.
* @return a character set name corresponding to the given character, defaulting
* to ASCII if no mapping could be made.
*/
public static CharacterSet valueOf( char designation )
{
for ( CharacterSet csn : values() )
{
if ( csn.isDesignation( designation ) )
{
return csn;
}
}
return ASCII;
}
/**
* Maps the character with the given index to a character in this character
* set.
*
* @param index the index of the character set, {@literal >= 0 && < 128}.
* @return a mapped character, or -1 if no mapping could be made and the
* ASCII value should be used.
*/
public abstract int map( int index );
/**
* Returns whether or not the given designation character belongs to this
* character set's set of designations.
*
* @param designation the designation to test for.
* @return {@code true} if the given designation character maps to this
* character set, {@code true} otherwise.
*/
private boolean isDesignation( char designation )
{
for (int myDesignation : myDesignations) {
if (myDesignation == designation) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,259 @@
/*
* Copyright 2022 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.test.jediterm.terminal.emulator.charset;
import org.springframework.shell.test.jediterm.terminal.util.CharUtils;
/**
* Provides the (graphical) character sets.
*
* @author jediterm authors
*/
public final class CharacterSets {
private static final int C0_START = 0;
private static final int C0_END = 31;
private static final int C1_START = 128;
private static final int C1_END = 159;
private static final int GL_START = 32;
private static final int GL_END = 127;
public static final String[] ASCII_NAMES = {"<nul>", "<soh>", "<stx>", "<etx>", "<eot>", "<enq>", "<ack>", "<bell>",
"\b", "\t", "\n", "<vt>", "<ff>", "\r", "<so>", "<si>", "<dle>", "<dc1>", "<dc2>", "<dc3>", "<dc4>", "<nak>",
"<syn>", "<etb>", "<can>", "<em>", "<sub>", "<esc>", "<fs>", "<gs>", "<rs>", "<us>", " ", "!", "\"", "#", "$",
"%", "&", "'", "(", ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":",
";", "<", "=", ">", "?", "@", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P",
"Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f",
"g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "{", "|",
"}", "~", "<del>"};
/**
* Denotes the mapping for C0 characters.
*/
public final static Object[][] C0_CHARS = {{0, "nul"}, //
{0, "soh"}, //
{0, "stx"}, //
{0, "etx"}, //
{0, "eot"}, //
{0, "enq"}, //
{0, "ack"}, //
{0, "bel"}, //
{(int) '\b', "bs"}, //
{(int) '\t', "ht"}, //
{(int) '\n', "lf"}, //
{0, "vt"}, //
{0, "ff"}, //
{(int) '\r', "cr"}, //
{0, "so"}, //
{0, "si"}, //
{0, "dle"}, //
{0, "dc1"}, //
{0, "dc2"}, //
{0, "dc3"}, //
{0, "dc4"}, //
{0, "nak"}, //
{0, "syn"}, //
{0, "etb"}, //
{0, "can"}, //
{0, "em"}, //
{0, "sub"}, //
{0, "esq"}, //
{0, "fs"}, //
{0, "gs"}, //
{0, "rs"}, //
{0, "us"}};
/**
* Denotes the mapping for C1 characters.
*/
public static final Object[][] C1_CHARS = {{0, null}, //
{0, null}, //
{0, null}, //
{0, null}, //
{0, "ind"}, //
{0, "nel"}, //
{0, "ssa"}, //
{0, "esa"}, //
{0, "hts"}, //
{0, "htj"}, //
{0, "vts"}, //
{0, "pld"}, //
{0, "plu"}, //
{0, "ri"}, //
{0, "ss2"}, //
{0, "ss3"}, //
{0, "dcs"}, //
{0, "pu1"}, //
{0, "pu2"}, //
{0, "sts"}, //
{0, "cch"}, //
{0, "mw"}, //
{0, "spa"}, //
{0, "epa"}, //
{0, null}, //
{0, null}, //
{0, null}, //
{0, "csi"}, //
{0, "st"}, //
{0, "osc"}, //
{0, "pm"}, //
{0, "apc"}};
/**
* The DEC special characters (only the last 32 characters).
* Contains [light][heavy] flavors for box drawing
*/
public static final Object[][] DEC_SPECIAL_CHARS = {{'\u25c6', null}, // black_diamond
{'\u2592', null}, // Medium Shade
{'\u2409', null}, // Horizontal tab (HT)
{'\u240c', null}, // Form Feed (FF)
{'\u240d', null}, // Carriage Return (CR)
{'\u240a', null}, // Line Feed (LF)
{'\u00b0', null}, // Degree sign
{'\u00b1', null}, // Plus/minus sign
{'\u2424', null}, // New Line (NL)
{'\u240b', null}, // Vertical Tab (VT)
{'\u2518', '\u251b'}, // Forms up and left
{'\u2510', '\u2513'}, // Forms down and left
{'\u250c', '\u250f'}, // Forms down and right
{'\u2514', '\u2517'}, // Forms up and right
{'\u253c', '\u254b'}, // Forms vertical and horizontal
{'\u23ba', null}, // Scan 1
{'\u23bb', null}, // Scan 3
{'\u2500', '\u2501'}, // Scan 5 / Horizontal bar
{'\u23bc', null}, // Scan 7
{'\u23bd', null}, // Scan 9
{'\u251c', '\u2523'}, // Forms vertical and right
{'\u2524', '\u252b'}, // Forms vertical and left
{'\u2534', '\u253b'}, // Forms up and horizontal
{'\u252c', '\u2533'}, // Forms down and horizontal
{'\u2502', '\u2503'}, // vertical bar
{'\u2264', null}, // less than or equal sign
{'\u2265', null}, // greater than or equal sign
{'\u03c0', null}, // pi
{'\u2260', null}, // not equal sign
{'\u00a3', null}, // pound sign
{'\u00b7', null}, // middle dot
{' ', null}, //
};
public static boolean isDecBoxChar(char c) {
if (c < '\u2500' || c >= '\u2580') { // fast path
return false;
}
for (Object[] o : DEC_SPECIAL_CHARS) {
if (c == (Character) o[0]) {
return true;
}
}
return false;
}
public static char getHeavyDecBoxChar(char c) {
if (c < '\u2500' || c >= '\u2580') { // fast path
return c;
}
for (Object[] o : DEC_SPECIAL_CHARS) {
if (c == (Character) o[0]) {
return o[1] != null ? (Character) o[1] : c;
}
}
return c;
}
/**
* Creates a new {@link CharacterSets} instance, never used.
*/
private CharacterSets() {
// Nop
}
// METHODS
/**
* Returns the character mapping for a given original value using the given
* graphic sets GL and GR.
*
* @param original the original character to map;
* @param gl the GL graphic set, cannot be <code>null</code>;
* @param gr the GR graphic set, cannot be <code>null</code>.
* @return the mapped character.
*/
public static char getChar(char original, GraphicSet gl, GraphicSet gr) {
Object[] mapping = getMapping(original, gl, gr);
int ch = (Integer) mapping[0];
if (ch > 0) {
return (char)ch;
}
return CharUtils.NUL_CHAR;
}
/**
* Returns the name for the given character using the given graphic sets GL
* and GR.
*
* @param original the original character to return the name for;
* @param gl the GL graphic set, cannot be <code>null</code>;
* @param gr the GR graphic set, cannot be <code>null</code>.
* @return the character name.
*/
public static String getCharName(char original, GraphicSet gl, GraphicSet gr) {
Object[] mapping = getMapping(original, gl, gr);
String name = (String)mapping[1];
if (name == null) {
name = String.format("<%d>", (int)original);
}
return name;
}
/**
* Returns the mapping for a given character using the given graphic sets GL
* and GR.
*
* @param original the original character to map;
* @param gl the GL graphic set, cannot be <code>null</code>;
* @param gr the GR graphic set, cannot be <code>null</code>.
* @return the mapped character.
*/
private static Object[] getMapping(char original, GraphicSet gl, GraphicSet gr) {
int mappedChar = original;
if (original >= C0_START && original <= C0_END) {
int idx = original - C0_START;
return C0_CHARS[idx];
}
else if (original >= C1_START && original <= C1_END) {
int idx = original - C1_START;
return C1_CHARS[idx];
}
else if (original >= GL_START && original <= GL_END) {
int idx = original - GL_START;
mappedChar = gl.map(original, idx);
}
//To support UTF-8 we don't use GR table
//TODO: verify that approach
//else if (original >= GR_START && original <= GR_END) {
// int idx = original - GR_START;
// mappedChar = gr.map(original, idx);
//}
return new Object[]{mappedChar, null};
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2022 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.test.jediterm.terminal.emulator.charset;
/**
* Denotes how a graphic set is designated.
*
* @author jediterm authors
*/
public class GraphicSet
{
private final int myIndex; // 0..3
private CharacterSet myDesignation;
public GraphicSet( int index )
{
if ( index < 0 || index > 3 )
{
throw new IllegalArgumentException( "Invalid index!" );
}
myIndex = index;
// The default mapping, based on XTerm...
myDesignation = CharacterSet.valueOf( ( index == 1 ) ? '0' : 'B' );
}
/**
* @return the designation of this graphic set.
*/
public CharacterSet getDesignation()
{
return myDesignation;
}
/**
* @return the index of this graphics set.
*/
public int getIndex()
{
return myIndex;
}
/**
* Maps a given character index to a concrete character.
*
* @param original
* the original character to map;
* @param index
* the index of the character to map.
* @return the mapped character, or the given original if no mapping could
* be made.
*/
public int map( char original, int index )
{
int result = myDesignation.map( index );
if ( result < 0 )
{
// No mapping, simply return the given original one...
result = original;
}
return result;
}
/**
* Sets the designation of this graphic set.
*/
public void setDesignation(CharacterSet designation )
{
myDesignation = designation;
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright 2022 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.test.jediterm.terminal.emulator.charset;
/**
* @author jediterm authors
*/
public class GraphicSetState {
private final GraphicSet[] myGraphicSets;
//in-use table graphic left (GL)
private GraphicSet myGL;
//in-use table graphic right (GR)
private GraphicSet myGR;
//Override for next char (used by shift-in and shift-out)
private GraphicSet myGlOverride;
public GraphicSetState() {
myGraphicSets = new GraphicSet[4];
for (int i = 0; i < myGraphicSets.length; i++) {
myGraphicSets[i] = new GraphicSet(i);
}
resetState();
}
/**
* Designates the given graphic set to the character set designator.
*
* @param graphicSet the graphic set to designate;
* @param designator the designator of the character set.
*/
public void designateGraphicSet( GraphicSet graphicSet, char designator) {
graphicSet.setDesignation(CharacterSet.valueOf(designator));
}
public void designateGraphicSet(int num, CharacterSet characterSet) {
getGraphicSet(num).setDesignation(characterSet);
}
/**
* Returns the (possibly overridden) GL graphic set.
*/
public GraphicSet getGL() {
GraphicSet result = myGL;
if (myGlOverride != null) {
result = myGlOverride;
myGlOverride = null;
}
return result;
}
/**
* Returns the GR graphic set.
*/
public GraphicSet getGR() {
return myGR;
}
/**
* Returns the current graphic set (one of four).
*
* @param index the index of the graphic set, 0..3.
*/
public GraphicSet getGraphicSet(int index) {
return myGraphicSets[index % 4];
}
/**
* Returns the mapping for the given character.
*
* @param ch the character to map.
* @return the mapped character.
*/
public char map(char ch) {
return CharacterSets.getChar(ch, getGL(), getGR());
}
/**
* Overrides the GL graphic set for the next written character.
*
* @param index the graphic set index, {@literal >= 0 && < 3}.
*/
public void overrideGL(int index) {
myGlOverride = getGraphicSet(index);
}
/**
* Resets the state to its initial values.
*/
public void resetState() {
for (int i = 0; i < myGraphicSets.length; i++) {
myGraphicSets[i].setDesignation(CharacterSet.valueOf((i == 1) ? '0' : 'B'));
}
myGL = myGraphicSets[0];
myGR = myGraphicSets[1];
myGlOverride = null;
}
/**
* Selects the graphic set for GL.
*
* @param index the graphic set index, {@literal >= 0 && <= 3}.
*/
public void setGL(int index) {
myGL = getGraphicSet(index);
}
/**
* Selects the graphic set for GR.
*
* @param index the graphic set index, {@literal >= 0 && <= 3}.
*/
public void setGR(int index) {
myGR = getGraphicSet(index);
}
public int getGLOverrideIndex() {
return myGlOverride != null ? myGlOverride.getIndex() : -1;
}
}

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
import java.util.Arrays;
import java.util.Iterator;
import org.springframework.shell.test.jediterm.terminal.util.CharUtils;
import org.springframework.shell.test.jediterm.terminal.util.Pair;
/**
* @author jediterm authors
*/
public class CharBuffer implements Iterable<Character>, CharSequence {
public final static CharBuffer EMPTY = new CharBuffer(new char[0], 0, 0);
private final char[] myBuf;
private final int myStart;
private final int myLength;
public CharBuffer( char[] buf, int start, int length) {
if (start + length > buf.length) {
throw new IllegalArgumentException(String.format("Out ouf bounds %d+%d>%d", start, length, buf.length));
}
myBuf = buf;
myStart = start;
myLength = length;
if (myLength < 0) {
throw new IllegalStateException("Length can't be negative: " + myLength);
}
if (myStart < 0) {
throw new IllegalStateException("Start position can't be negative: " + myStart);
}
if (myStart + myLength > myBuf.length) {
throw new IllegalStateException(String.format("Interval is out of array bounds: %d+%d>%d", myStart, myLength, myBuf.length));
}
}
public CharBuffer(char c, int count) {
this(new char[count], 0, count);
assert !CharUtils.isDoubleWidthCharacter(c, false);
Arrays.fill(myBuf, c);
}
public CharBuffer( String str) {
this(str.toCharArray(), 0, str.length());
}
@Override
public Iterator<Character> iterator() {
return new Iterator<Character>() {
private int myCurPosition = myStart;
@Override
public boolean hasNext() {
return myCurPosition < myBuf.length && myCurPosition < myStart + myLength;
}
@Override
public Character next() {
return myBuf[myCurPosition];
}
@Override
public void remove() {
throw new IllegalStateException("Can't remove from buffer");
}
};
}
public char[] getBuf() {
return myBuf;
}
public int getStart() {
return myStart;
}
public CharBuffer subBuffer(int start, int length) {
return new CharBuffer(myBuf, getStart() + start, length);
}
public CharBuffer subBuffer(Pair<Integer, Integer> range) {
return new CharBuffer(myBuf, getStart() + range.first, range.second - range.first);
}
public boolean isNul() {
return myLength > 0 && myBuf[0] == CharUtils.NUL_CHAR;
}
public void unNullify() {
Arrays.fill(myBuf, CharUtils.EMPTY_CHAR);
}
@Override
public int length() {
return myLength;
}
@Override
public char charAt(int index) {
return myBuf[myStart + index];
}
@Override
public CharSequence subSequence(int start, int end) {
return new CharBuffer(myBuf, myStart + start, end - start);
}
@Override
public String toString() {
return new String(myBuf, myStart, myLength);
}
public CharBuffer clone() {
char[] newBuf = Arrays.copyOfRange(myBuf, myStart, myStart + myLength);
return new CharBuffer(newBuf, 0, myLength);
}
}

View File

@@ -0,0 +1,285 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.shell.test.jediterm.terminal.StyledTextConsumer;
import org.springframework.shell.test.jediterm.terminal.TextStyle;
import org.springframework.shell.test.jediterm.terminal.model.TerminalLine.TextEntry;
import java.util.ArrayList;
import java.util.List;
/**
* Holds styled characters lines
*
* @author jediterm authors
*/
public class LinesBuffer {
private static final Logger LOG = LoggerFactory.getLogger(LinesBuffer.class);
public static final int DEFAULT_MAX_LINES_COUNT = 5000;
// negative number means no limit
private int myBufferMaxLinesCount = DEFAULT_MAX_LINES_COUNT;
private ArrayList<TerminalLine> myLines = new ArrayList<>();
public LinesBuffer() {
}
public LinesBuffer(int bufferMaxLinesCount) {
myBufferMaxLinesCount = bufferMaxLinesCount;
}
public synchronized String getLines() {
final StringBuilder sb = new StringBuilder();
boolean first = true;
for (TerminalLine line : myLines) {
if (!first) {
sb.append("\n");
}
sb.append(line.getText());
first = false;
}
return sb.toString();
}
public synchronized void addNewLine( TextStyle style, CharBuffer characters) {
addNewLine(new TerminalLine.TextEntry(style, characters));
}
private synchronized void addNewLine( TerminalLine.TextEntry entry) {
addLine(new TerminalLine(entry));
}
private synchronized void addLine( TerminalLine line) {
if (myBufferMaxLinesCount > 0 && myLines.size() >= myBufferMaxLinesCount) {
removeTopLines(1);
}
myLines.add(line);
}
public synchronized int getLineCount() {
return myLines.size();
}
public synchronized void removeTopLines(int count) {
if (count >= myLines.size()) { // remove all lines
myLines = new ArrayList<>();
} else {
myLines = new ArrayList<>(myLines.subList(count, myLines.size()));
}
}
public String getLineText(int row) {
TerminalLine line = getLine(row);
return line.getText();
}
public synchronized void insertLines(int y, int count, int lastLine, TextEntry filler) {
LinesBuffer tail = new LinesBuffer();
if (lastLine < getLineCount() - 1) {
moveBottomLinesTo(getLineCount() - lastLine - 1, tail);
}
LinesBuffer head = new LinesBuffer();
if (y > 0) {
moveTopLinesTo(y, head);
}
for (int i = 0; i < count; i++) {
head.addNewLine(filler);
}
head.moveBottomLinesTo(head.getLineCount(), this);
removeBottomLines(count);
tail.moveTopLinesTo(tail.getLineCount(), this);
}
public synchronized LinesBuffer deleteLines(int y, int count, int lastLine, TextEntry filler) {
LinesBuffer tail = new LinesBuffer();
if (lastLine < getLineCount() - 1) {
moveBottomLinesTo(getLineCount() - lastLine - 1, tail);
}
LinesBuffer head = new LinesBuffer();
if (y > 0) {
moveTopLinesTo(y, head);
}
int toRemove = Math.min(count, getLineCount());
LinesBuffer removed = new LinesBuffer();
moveTopLinesTo(toRemove, removed);
head.moveBottomLinesTo(head.getLineCount(), this);
for (int i = 0; i < toRemove; i++) {
addNewLine(filler);
}
tail.moveTopLinesTo(tail.getLineCount(), this);
return removed;
}
public synchronized void writeString(int x, int y, CharBuffer str, TextStyle style) {
TerminalLine line = getLine(y);
line.writeString(x, str, style);
// if (myTextProcessing != null) {
// myTextProcessing.processHyperlinks(this, line);
// }
}
public synchronized void clearLines(int startRow, int endRow, TextEntry filler) {
for (int i = startRow; i <= endRow; i++) {
getLine(i).clear(filler);
}
}
// used for reset, style not needed here (reset as well)
public synchronized void clearAll() {
myLines.clear();
}
public synchronized void deleteCharacters(int x, int y, int count, TextStyle style) {
TerminalLine line = getLine(y);
line.deleteCharacters(x, count, style);
}
public synchronized void insertBlankCharacters(final int x, final int y, final int count, final int maxLen, TextStyle style) {
TerminalLine line = getLine(y);
line.insertBlankCharacters(x, count, maxLen, style);
}
public synchronized void clearArea(int leftX, int topY, int rightX, int bottomY, TextStyle style) {
for (int y = topY; y < bottomY; y++) {
TerminalLine line = getLine(y);
line.clearArea(leftX, rightX, style);
}
}
public synchronized void processLines(final int yStart, final int yCount, final StyledTextConsumer consumer) {
processLines(yStart, yCount, consumer, -getLineCount());
}
public synchronized void processLines(final int firstLine,
final int count,
final StyledTextConsumer consumer,
final int startRow) {
if (firstLine<0) {
throw new IllegalArgumentException("firstLine=" + firstLine + ", should be >0");
}
for (int y = firstLine; y < Math.min(firstLine + count, myLines.size()); y++) {
myLines.get(y).process(y, consumer, startRow);
}
}
public synchronized void moveTopLinesTo(int count, final LinesBuffer buffer) {
count = Math.min(count, getLineCount());
buffer.addLines(myLines.subList(0, count));
removeTopLines(count);
}
public synchronized void addLines( List<TerminalLine> lines) {
if (myBufferMaxLinesCount > 0) {
// adding more lines than max size
if (lines.size() >= myBufferMaxLinesCount) {
int index = lines.size() - myBufferMaxLinesCount;
myLines = new ArrayList<>(lines.subList(index, lines.size()));
return;
}
int count = myLines.size() + lines.size();
if (count >= myBufferMaxLinesCount) {
removeTopLines(count - myBufferMaxLinesCount);
}
}
myLines.addAll(lines);
}
public synchronized TerminalLine getLine(int row) {
if (row<0) {
LOG.error("Negative line number: " + row);
return TerminalLine.createEmpty();
}
for (int i = getLineCount(); i <= row; i++) {
addLine(TerminalLine.createEmpty());
}
return myLines.get(row);
}
public synchronized void moveBottomLinesTo(int count, final LinesBuffer buffer) {
count = Math.min(count, getLineCount());
buffer.addLinesFirst(myLines.subList(getLineCount() - count, getLineCount()));
removeBottomLines(count);
}
private synchronized void addLinesFirst( List<TerminalLine> lines) {
List<TerminalLine> list = new ArrayList<>(lines);
list.addAll(myLines);
myLines = new ArrayList<>(list);
}
private synchronized void removeBottomLines(int count) {
myLines = new ArrayList<>(myLines.subList(0, getLineCount() - count));
}
public int removeBottomEmptyLines(int ind, int maxCount) {
int i = 0;
while ((maxCount - i) > 0 && (ind >= myLines.size() || myLines.get(ind).isNul())) {
if (ind < myLines.size()) {
myLines.remove(ind);
}
ind--;
i++;
}
return i;
}
synchronized int findLineIndex( TerminalLine line) {
return myLines.indexOf(line);
}
public synchronized void clearTypeAheadPredictions() {
for (TerminalLine line : myLines) {
line.myTypeAheadLine = null;
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
import org.springframework.shell.test.jediterm.terminal.TextStyle;
import org.springframework.shell.test.jediterm.terminal.emulator.charset.CharacterSet;
import org.springframework.shell.test.jediterm.terminal.emulator.charset.GraphicSetState;
/**
* @author jediterm authors
*/
public class StoredCursor {
//Cursor position
private final int myCursorX;
private final int myCursorY;
//Character attributes set by the SGR command
private final TextStyle myTextStyle;
//Character sets (G0, G1, G2, or G3) currently in GL and GR
private final int myGLMapping;
private final int myGRMapping;
//Wrap flag (autowrap or no autowrap)
private final boolean myAutoWrap;
//State of origin mode (DECOM)
private final boolean myOriginMode;
//Selective erase attribute
//Any single shift 2 (SS2) or single shift 3 (SS3) functions sent
private final int myGLOverride;
private final CharacterSet[] myDesignations = new CharacterSet[4];
public StoredCursor(int cursorX,
int cursorY,
TextStyle textStyle,
boolean autoWrap,
boolean originMode,
GraphicSetState graphicSetState) {
myCursorX = cursorX;
myCursorY = cursorY;
myTextStyle = textStyle;
myAutoWrap = autoWrap;
myOriginMode = originMode;
myGLMapping = graphicSetState.getGL().getIndex();
myGRMapping = graphicSetState.getGR().getIndex();
myGLOverride = graphicSetState.getGLOverrideIndex();
for (int i = 0; i<4; i++) {
myDesignations[i] = graphicSetState.getGraphicSet(i).getDesignation();
}
}
public int getCursorX() {
return myCursorX;
}
public int getCursorY() {
return myCursorY;
}
public TextStyle getTextStyle() {
return myTextStyle;
}
public int getGLMapping() {
return myGLMapping;
}
public int getGRMapping() {
return myGRMapping;
}
public boolean isAutoWrap() {
return myAutoWrap;
}
public boolean isOriginMode() {
return myOriginMode;
}
public int getGLOverride() {
return myGLOverride;
}
public CharacterSet[] getDesignations() {
return myDesignations;
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
import org.springframework.shell.test.jediterm.terminal.TextStyle;
/**
* @author jediterm authors
*/
public class StyleState {
private TextStyle myCurrentStyle = TextStyle.EMPTY;
private TextStyle myDefaultStyle = TextStyle.EMPTY;
private TextStyle myMergedStyle = null;
public StyleState() {
}
public TextStyle getCurrent() {
return TextStyle.getCanonicalStyle(getMergedStyle());
}
private static TextStyle merge( TextStyle style, TextStyle defaultStyle) {
TextStyle.Builder builder = style.toBuilder();
// if (style.getBackground() == null && defaultStyle.getBackground() != null) {
// builder.setBackground(defaultStyle.getBackground());
// }
// if (style.getForeground() == null && defaultStyle.getForeground() != null) {
// builder.setForeground(defaultStyle.getForeground());
// }
return builder.build();
}
public void reset() {
myCurrentStyle = myDefaultStyle;
myMergedStyle = null;
}
public void set(StyleState styleState) {
setCurrent(styleState.getCurrent());
}
public void setDefaultStyle(TextStyle defaultStyle) {
myDefaultStyle = defaultStyle;
myMergedStyle = null;
}
// public TerminalColor getBackground() {
// return getBackground(null);
// }
// public TerminalColor getBackground(TerminalColor color) {
// return color != null ? color : myDefaultStyle.getBackground();
// }
// public TerminalColor getForeground() {
// return getForeground(null);
// }
// public TerminalColor getForeground(TerminalColor color) {
// return color != null ? color : myDefaultStyle.getForeground();
// }
public void setCurrent(TextStyle current) {
myCurrentStyle = current;
myMergedStyle = null;
}
private TextStyle getMergedStyle() {
if (myMergedStyle == null) {
myMergedStyle = merge(myCurrentStyle, myDefaultStyle);
}
return myMergedStyle;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
/**
* @author jediterm authors
*/
public class SubCharBuffer extends CharBuffer {
private final CharBuffer myParent;
private final int myOffset;
public SubCharBuffer( CharBuffer parent, int offset, int length) {
super(parent.getBuf(), parent.getStart() + offset, length);
myParent = parent;
myOffset = offset;
}
public CharBuffer getParent() {
return myParent;
}
public int getOffset() {
return myOffset;
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
/**
* Provides a tabulator that keeps track of the tab stops of a terminal.
*
* @author jediterm authors
*/
public interface Tabulator
{
/**
* Clears the tab stop at the given position.
*
* @param position
* the column position used to determine the next tab stop, > 0.
*/
void clearTabStop(int position);
/**
* Clears all tab stops.
*/
void clearAllTabStops();
/**
* Returns the width of the tab stop that is at or after the given position.
*
* @param position
* the column position used to determine the next tab stop, >= 0.
* @return the next tab stop width, >= 0.
*/
int getNextTabWidth(int position);
/**
* Returns the width of the tab stop that is before the given position.
*
* @param position
* the column position used to determine the previous tab stop, >= 0.
* @return the previous tab stop width, >= 0.
*/
int getPreviousTabWidth(int position);
/**
* Returns the next tab stop that is at or after the given position.
*
* @param position
* the column position used to determine the next tab stop, >= 0.
* @return the next tab stop, >= 0.
*/
int nextTab(int position);
/**
* Returns the previous tab stop that is before the given position.
*
* @param position
* the column position used to determine the previous tab stop, >= 0.
* @return the previous tab stop, >= 0.
*/
int previousTab(int position);
/**
* Sets the tab stop to the given position.
*
* @param position
* the position of the (new) tab stop, > 0.
*/
void setTabStop(int position);
void resize(int width);
}

View File

@@ -0,0 +1,464 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.shell.test.jediterm.terminal.StyledTextConsumer;
import org.springframework.shell.test.jediterm.terminal.TextStyle;
import org.springframework.shell.test.jediterm.terminal.util.CharUtils;
import org.springframework.shell.test.jediterm.terminal.util.Pair;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* @author jediterm authors
*/
public final class TerminalLine {
private static final Logger LOG = LoggerFactory.getLogger(TerminalLine.class);
private TextEntries myTextEntries = new TextEntries();
private boolean myWrapped = false;
private final List<TerminalLineIntervalHighlighting> myCustomHighlightings = new ArrayList<>();
TerminalLine myTypeAheadLine;
public TerminalLine() {
}
public TerminalLine( TextEntry entry) {
myTextEntries.add(entry);
}
public static TerminalLine createEmpty() {
return new TerminalLine();
}
public String getText() {
StringBuilder result = new StringBuilder(myTextEntries.myLength);
for (TerminalLine.TextEntry textEntry : myTextEntries) {
// NUL can only be at the end
if (textEntry.getText().isNul()) {
break;
}
result.append(textEntry.getText());
}
return result.toString();
}
public TerminalLine copy() {
TerminalLine result = new TerminalLine();
for (TextEntry entry : myTextEntries) {
result.myTextEntries.add(entry);
}
result.myWrapped = myWrapped;
return result;
}
public char charAt(int x) {
TerminalLine typeAheadLine = myTypeAheadLine;
if (typeAheadLine != null) {
return typeAheadLine.charAt(x);
}
String text = getText();
return x < text.length() ? text.charAt(x) : CharUtils.EMPTY_CHAR;
}
public boolean isWrapped() {
return myWrapped;
}
public void setWrapped(boolean wrapped) {
myWrapped = wrapped;
}
public synchronized void clear( TextEntry filler) {
myTextEntries.clear();
myTextEntries.add(filler);
setWrapped(false);
}
public void writeString(int x, CharBuffer str, TextStyle style) {
writeCharacters(x, style, str);
}
public void insertString(int x, CharBuffer str, TextStyle style) {
insertCharacters(x, style, str);
}
private synchronized void writeCharacters(int x, TextStyle style, CharBuffer characters) {
int len = myTextEntries.length();
if (x >= len) {
// fill the gap
if (x - len > 0) {
myTextEntries.add(new TextEntry(TextStyle.EMPTY, new CharBuffer(CharUtils.NUL_CHAR, x - len)));
}
myTextEntries.add(new TextEntry(style, characters));
} else {
len = Math.max(len, x + characters.length());
myTextEntries = merge(x, characters, style, myTextEntries, len);
}
}
private synchronized void insertCharacters(int x, TextStyle style, CharBuffer characters) {
int length = myTextEntries.length();
if (x > length) {
writeCharacters(x, style, characters);
return;
}
Pair<char[], TextStyle[]> pair = toBuf(myTextEntries, length + characters.length());
for (int i = length - 1; i >= x; i--) {
pair.first[i + characters.length()] = pair.first[i];
pair.second[i + characters.length()] = pair.second[i];
}
for (int i = 0; i < characters.length(); i++) {
pair.first[i + x] = characters.charAt(i);
pair.second[i + x] = style;
}
myTextEntries = collectFromBuffer(pair.first, pair.second);
}
private static TextEntries merge(int x, CharBuffer str, TextStyle style, TextEntries entries, int lineLength) {
Pair<char[], TextStyle[]> pair = toBuf(entries, lineLength);
for (int i = 0; i < str.length(); i++) {
pair.first[i + x] = str.charAt(i);
pair.second[i + x] = style;
}
return collectFromBuffer(pair.first, pair.second);
}
private static Pair<char[], TextStyle[]> toBuf(TextEntries entries, int lineLength) {
Pair<char[], TextStyle[]> pair = Pair.create(new char[lineLength], new TextStyle[lineLength]);
int p = 0;
for (TextEntry entry : entries) {
for (int i = 0; i < entry.getLength(); i++) {
pair.first[p + i] = entry.getText().charAt(i);
pair.second[p + i] = entry.getStyle();
}
p += entry.getLength();
}
return pair;
}
private static TextEntries collectFromBuffer(char[] buf, TextStyle[] styles) {
TextEntries result = new TextEntries();
TextStyle curStyle = styles[0];
int start = 0;
for (int i = 1; i < buf.length; i++) {
if (styles[i] != curStyle) {
result.add(new TextEntry(curStyle, new CharBuffer(buf, start, i - start)));
curStyle = styles[i];
start = i;
}
}
result.add(new TextEntry(curStyle, new CharBuffer(buf, start, buf.length - start)));
return result;
}
public synchronized void deleteCharacters(int x) {
deleteCharacters(x, TextStyle.EMPTY);
}
public synchronized void deleteCharacters(int x, TextStyle style) {
deleteCharacters(x, myTextEntries.length() - x, style);
// delete to the end of line : line is no more wrapped
setWrapped(false);
}
public synchronized void deleteCharacters(int x, int count, TextStyle style) {
int p = 0;
TextEntries newEntries = new TextEntries();
int remaining = count;
for (TextEntry entry : myTextEntries) {
if (remaining == 0) {
newEntries.add(entry);
continue;
}
int len = entry.getLength();
if (p + len <= x) {
p += len;
newEntries.add(entry);
continue;
}
int dx = x - p; //>=0
if (dx > 0) {
//part of entry before x
newEntries.add(new TextEntry(entry.getStyle(), entry.getText().subBuffer(0, dx)));
p = x;
}
if (dx + remaining < len) {
//part that left after deleting count
newEntries.add(new TextEntry(entry.getStyle(), entry.getText().subBuffer(dx + remaining, len - (dx + remaining))));
remaining = 0;
} else {
remaining -= (len - dx);
p = x;
}
}
if (count > 0 && style != TextStyle.EMPTY) { // apply style to the end of the line
newEntries.add(new TextEntry(style, new CharBuffer(CharUtils.NUL_CHAR, count)));
}
myTextEntries = newEntries;
}
public synchronized void insertBlankCharacters(int x, int count, int maxLen, TextStyle style) {
int len = myTextEntries.length();
len = Math.min(len + count, maxLen);
char[] buf = new char[len];
TextStyle[] styles = new TextStyle[len];
int p = 0;
for (TextEntry entry : myTextEntries) {
for (int i = 0; i < entry.getLength() && p < len; i++) {
if (p == x) {
for (int j = 0; j < count && p < len; j++) {
buf[p] = CharUtils.EMPTY_CHAR;
styles[p] = style;
p++;
}
}
if (p < len) {
buf[p] = entry.getText().charAt(i);
styles[p] = entry.getStyle();
p++;
}
}
if (p >= len) {
break;
}
}
// if not inserted yet (ie. x > len)
for (; p < x && p < len; p++) {
buf[p] = CharUtils.EMPTY_CHAR;
styles[p] = TextStyle.EMPTY;
p++;
}
for (; p < x + count && p < len; p++) {
buf[p] = CharUtils.EMPTY_CHAR;
styles[p] = style;
p++;
}
myTextEntries = collectFromBuffer(buf, styles);
}
public synchronized void clearArea(int leftX, int rightX, TextStyle style) {
if (rightX == -1) {
rightX = Math.max(myTextEntries.length(), leftX);
}
writeCharacters(leftX, style, new CharBuffer(
rightX >= myTextEntries.length() ? CharUtils.NUL_CHAR : CharUtils.EMPTY_CHAR,
rightX - leftX));
}
public synchronized TextStyle getStyleAt(int x) {
int i = 0;
for (TextEntry te : myTextEntries) {
if (x >= i && x < i + te.getLength()) {
return te.getStyle();
}
i += te.getLength();
}
return null;
}
public synchronized void process(int y, StyledTextConsumer consumer, int startRow) {
int x = 0;
int nulIndex = -1;
TerminalLineIntervalHighlighting highlighting = myCustomHighlightings.stream().findFirst().orElse(null);
TerminalLine typeAheadLine = myTypeAheadLine;
TextEntries textEntries = typeAheadLine != null ? typeAheadLine.myTextEntries : myTextEntries;
for (TextEntry te : textEntries) {
if (te.getText().isNul()) {
if (nulIndex < 0) {
nulIndex = x;
}
consumer.consumeNul(x, y, nulIndex, te.getStyle(), te.getText(), startRow);
} else {
if (highlighting != null && te.getLength() > 0 && highlighting.intersectsWith(x, x + te.getLength())) {
processIntersection(x, y, te, consumer, startRow, highlighting);
}
else {
consumer.consume(x, y, te.getStyle(), te.getText(), startRow);
}
}
x += te.getLength();
}
consumer.consumeQueue(x, y, nulIndex < 0 ? x : nulIndex, startRow);
}
private void processIntersection(int startTextOffset, int y, TextEntry te, StyledTextConsumer consumer,
int startRow, TerminalLineIntervalHighlighting highlighting) {
CharBuffer text = te.getText();
int endTextOffset = startTextOffset + text.length();
int[] offsets = new int[] {startTextOffset, endTextOffset, highlighting.getStartOffset(), highlighting.getEndOffset()};
Arrays.sort(offsets);
int startTextOffsetInd = Arrays.binarySearch(offsets, startTextOffset);
int endTextOffsetInd = Arrays.binarySearch(offsets, endTextOffset);
if (startTextOffsetInd < 0 || endTextOffsetInd < 0) {
LOG.error("Cannot find " + Arrays.toString(new int[] {startTextOffset, endTextOffset})
+ " in " + Arrays.toString(offsets) + ": " + Arrays.toString(new int[] {startTextOffsetInd, endTextOffsetInd}));
consumer.consume(startTextOffset, y, te.getStyle(), text, startRow);
return;
}
for (int i = startTextOffsetInd; i < endTextOffsetInd; i++) {
int length = offsets[i + 1] - offsets[i];
if (length == 0) continue;
CharBuffer subText = new SubCharBuffer(text, offsets[i] - startTextOffset, length);
if (highlighting.intersectsWith(offsets[i], offsets[i + 1])) {
consumer.consume(offsets[i], y, highlighting.mergeWith(te.getStyle()), subText, startRow);
}
else {
consumer.consume(offsets[i], y, te.getStyle(), subText, startRow);
}
}
}
public synchronized boolean isNul() {
for (TextEntry e : myTextEntries) {
if (!e.isNul()) {
return false;
}
}
return true;
}
void forEachEntry( Consumer<TextEntry> action) {
myTextEntries.forEach(action);
}
// @TestOnly
public List<TextEntry> getEntries() {
return Collections.unmodifiableList(myTextEntries.entries());
}
void appendEntry( TextEntry entry) {
myTextEntries.add(entry);
}
// @SuppressWarnings("unused") // used by IntelliJ
// public synchronized TerminalLineIntervalHighlighting addCustomHighlighting(int startOffset, int length, TextStyle textStyle) {
// TerminalLineIntervalHighlighting highlighting = new TerminalLineIntervalHighlighting(this, startOffset, length, textStyle) {
// @Override
// protected void doDispose() {
// synchronized (TerminalLine.this) {
// myCustomHighlightings.remove(this);
// }
// }
// };
// myCustomHighlightings.add(highlighting);
// return highlighting;
// }
@Override
public String toString() {
return myTextEntries.length() + " chars, " +
(myWrapped ? "wrapped, " : "") +
myTextEntries.myTextEntries.size() + " entries: " +
myTextEntries.myTextEntries.stream()
.map(entry -> entry.getText().toString())
.collect(Collectors.joining("|"));
}
public static class TextEntry {
private final TextStyle myStyle;
private final CharBuffer myText;
public TextEntry( TextStyle style, CharBuffer text) {
myStyle = style;
myText = text.clone();
}
public TextStyle getStyle() {
return myStyle;
}
public CharBuffer getText() {
return myText;
}
public int getLength() {
return myText.length();
}
public boolean isNul() {
return myText.isNul();
}
@Override
public String toString() {
return myText.length() + " chars, style: " + myStyle + ", text: " + myText;
}
}
private static class TextEntries implements Iterable<TextEntry> {
private final List<TextEntry> myTextEntries = new ArrayList<>();
private int myLength = 0;
public void add(TextEntry entry) {
// NUL can only be at the end of the line
if (!entry.getText().isNul()) {
for (TextEntry t : myTextEntries) {
if (t.getText().isNul()) {
t.getText().unNullify();
}
}
}
myTextEntries.add(entry);
myLength += entry.getLength();
}
private List<TextEntry> entries() {
return myTextEntries;
}
public Iterator<TextEntry> iterator() {
return myTextEntries.iterator();
}
public int length() {
return myLength;
}
public void clear() {
myTextEntries.clear();
myLength = 0;
}
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
import org.springframework.shell.test.jediterm.terminal.TextStyle;
/**
* @author jediterm authors
*/
public abstract class TerminalLineIntervalHighlighting {
private final TerminalLine myLine;
private final int myStartOffset;
private final int myEndOffset;
private boolean myDisposed = false;
TerminalLineIntervalHighlighting( TerminalLine line, int startOffset, int length) {
if (startOffset < 0) {
throw new IllegalArgumentException("Negative startOffset: " + startOffset);
}
if (length < 0) {
throw new IllegalArgumentException("Negative length: " + length);
}
myLine = line;
myStartOffset = startOffset;
myEndOffset = startOffset + length;
}
public TerminalLine getLine() {
return myLine;
}
public int getStartOffset() {
return myStartOffset;
}
public int getEndOffset() {
return myEndOffset;
}
public int getLength() {
return myEndOffset - myStartOffset;
}
public boolean isDisposed() {
return myDisposed;
}
public final void dispose() {
doDispose();
myDisposed = true;
}
protected abstract void doDispose();
public boolean intersectsWith(int otherStartOffset, int otherEndOffset) {
return !(myEndOffset <= otherStartOffset || otherEndOffset <= myStartOffset);
}
public TextStyle mergeWith( TextStyle style) {
// TerminalColor foreground = myStyle.getForeground();
// if (foreground == null) {
// foreground = style.getForeground();
// }
// TerminalColor background = myStyle.getBackground();
// if (background == null) {
// background = style.getBackground();
// }
return new TextStyle();
}
@Override
public String toString() {
return "startOffset=" + myStartOffset +
", endOffset=" + myEndOffset +
", disposed=" + myDisposed;
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
/**
* @author jediterm authors
*/
public interface TerminalModelListener {
void modelChanged();
}

View File

@@ -0,0 +1,499 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.shell.test.jediterm.terminal.StyledTextConsumer;
import org.springframework.shell.test.jediterm.terminal.StyledTextConsumerAdapter;
import org.springframework.shell.test.jediterm.terminal.TextStyle;
import org.springframework.shell.test.jediterm.terminal.model.TerminalLine.TextEntry;
import org.springframework.shell.test.jediterm.terminal.util.CharUtils;
import org.springframework.shell.test.jediterm.terminal.util.Pair;
/**
* Buffer for storing styled text data.
* Stores only text that fit into one screen XxY, but has scrollBuffer to save history lines and screenBuffer to restore
* screen after resize. ScrollBuffer stores all lines before the first line currently shown on the screen. TextBuffer
* stores lines that are shown currently on the screen and they have there(in TextBuffer) their initial length (even if
* it doesn't fit to screen width).
* <p/>
*
* @author jediterm authors
*/
public class TerminalTextBuffer {
private static final Logger LOG = LoggerFactory.getLogger(TerminalTextBuffer.class);
private final StyleState myStyleState;
private LinesBuffer myHistoryBuffer;
private LinesBuffer myScreenBuffer;
private int myWidth;
private int myHeight;
private final int myHistoryLinesCount;
private final Lock myLock = new ReentrantLock();
private LinesBuffer myHistoryBufferBackup;
private LinesBuffer myScreenBufferBackup; // to store textBuffer after switching to alternate buffer
// private boolean myAlternateBuffer = false;
private boolean myUsingAlternateBuffer = false;
private final List<TerminalModelListener> myListeners = new CopyOnWriteArrayList<>();
private final List<TerminalModelListener> myTypeAheadListeners = new CopyOnWriteArrayList<>();
public TerminalTextBuffer(final int width, final int height, StyleState styleState) {
this(width, height, styleState, LinesBuffer.DEFAULT_MAX_LINES_COUNT);
}
public TerminalTextBuffer(final int width, final int height, StyleState styleState, final int historyLinesCount) {
myStyleState = styleState;
myWidth = width;
myHeight = height;
myHistoryLinesCount = historyLinesCount;
myScreenBuffer = createScreenBuffer();
myHistoryBuffer = createHistoryBuffer();
}
private LinesBuffer createScreenBuffer() {
return new LinesBuffer(-1);
}
private LinesBuffer createHistoryBuffer() {
return new LinesBuffer(myHistoryLinesCount);
}
// public Dimension resize(final Dimension pendingResize, final RequestOrigin origin, final int cursorX,
// final int cursorY, JediTerminal.ResizeHandler resizeHandler) {
// lock();
// try {
// return doResize(pendingResize, origin, cursorX, cursorY, resizeHandler);
// } finally {
// unlock();
// }
// }
// private Dimension doResize(final Dimension pendingResize, final RequestOrigin origin, final int cursorX,
// final int cursorY, JediTerminal.ResizeHandler resizeHandler) {
// final int newWidth = pendingResize.width;
// final int newHeight = pendingResize.height;
// int newCursorX = cursorX;
// int newCursorY = cursorY;
// if (myWidth != newWidth) {
// myWidth = newWidth;
// myHeight = newHeight;
// }
// final int oldHeight = myHeight;
// if (newHeight < oldHeight) {
// int count = oldHeight - newHeight;
// if (!myAlternateBuffer) {
// //we need to move lines from text buffer to the scroll buffer
// //but empty bottom lines can be collapsed
// int emptyLinesDeleted = myScreenBuffer.removeBottomEmptyLines(oldHeight - 1, count);
// myScreenBuffer.moveTopLinesTo(count - emptyLinesDeleted, myHistoryBuffer);
// newCursorY = cursorY - (count - emptyLinesDeleted);
// } else {
// newCursorY = cursorY;
// }
// } else if (newHeight > oldHeight) {
// if (!myAlternateBuffer) {
// //we need to move lines from scroll buffer to the text buffer
// int historyLinesCount = Math.min(newHeight - oldHeight, myHistoryBuffer.getLineCount());
// myHistoryBuffer.moveBottomLinesTo(historyLinesCount, myScreenBuffer);
// newCursorY = cursorY + historyLinesCount;
// } else {
// newCursorY = cursorY;
// }
// }
// myWidth = newWidth;
// myHeight = newHeight;
// resizeHandler.sizeUpdated(myWidth, myHeight, newCursorX, newCursorY);
// fireModelChangeEvent();
// return pendingResize;
// }
public void addModelListener(TerminalModelListener listener) {
myListeners.add(listener);
}
public void addTypeAheadModelListener(TerminalModelListener listener) {
myTypeAheadListeners.add(listener);
}
public void removeModelListener(TerminalModelListener listener) {
myListeners.remove(listener);
}
public void removeTypeAheadModelListener(TerminalModelListener listener) {
myTypeAheadListeners.remove(listener);
}
void fireModelChangeEvent() {
for (TerminalModelListener modelListener : myListeners) {
modelListener.modelChanged();
}
}
void fireTypeAheadModelChangeEvent() {
for (TerminalModelListener modelListener : myTypeAheadListeners) {
modelListener.modelChanged();
}
}
private TextStyle createEmptyStyleWithCurrentColor() {
return myStyleState.getCurrent().createEmptyWithColors();
}
private TextEntry createFillerEntry() {
return new TextEntry(createEmptyStyleWithCurrentColor(), new CharBuffer(CharUtils.NUL_CHAR, myWidth));
}
public void deleteCharacters(final int x, final int y, final int count) {
if (y > myHeight - 1 || y < 0) {
LOG.error("attempt to delete in line " + y + "\n" +
"args were x:" + x + " count:" + count);
} else if (count < 0) {
LOG.error("Attempt to delete negative chars number: count:" + count);
} else if (count > 0) {
myScreenBuffer.deleteCharacters(x, y, count, createEmptyStyleWithCurrentColor());
fireModelChangeEvent();
}
}
public void insertBlankCharacters(final int x, final int y, final int count) {
if (y > myHeight - 1 || y < 0) {
LOG.error("attempt to insert blank chars in line " + y + "\n" +
"args were x:" + x + " count:" + count);
} else if (count < 0) {
LOG.error("Attempt to insert negative blank chars number: count:" + count);
} else if (count > 0) { //nothing to do
myScreenBuffer.insertBlankCharacters(x, y, count, myWidth, createEmptyStyleWithCurrentColor());
fireModelChangeEvent();
}
}
public void writeString(final int x, final int y, final CharBuffer str) {
writeString(x, y, str, myStyleState.getCurrent());
}
public void addLine( final TerminalLine line) {
myScreenBuffer.addLines(List.of(line));
fireModelChangeEvent();
}
private void writeString(int x, int y, CharBuffer str, TextStyle style) {
myScreenBuffer.writeString(x, y - 1, str, style);
fireModelChangeEvent();
}
public void scrollArea(final int scrollRegionTop, final int dy, int scrollRegionBottom) {
if (dy == 0) {
return;
}
if (dy > 0) {
insertLines(scrollRegionTop - 1, dy, scrollRegionBottom);
} else {
LinesBuffer removed = deleteLines(scrollRegionTop - 1, -dy, scrollRegionBottom);
if (scrollRegionTop == 1) {
removed.moveTopLinesTo(removed.getLineCount(), myHistoryBuffer);
}
fireModelChangeEvent();
}
}
public String getStyleLines() {
final Map<Integer, Integer> hashMap = new HashMap<>();
myLock.lock();
try {
final StringBuilder sb = new StringBuilder();
myScreenBuffer.processLines(0, myHeight, new StyledTextConsumerAdapter() {
int count = 0;
@Override
public void consume(int x, int y, TextStyle style, CharBuffer characters, int startRow) {
if (x == 0) {
sb.append("\n");
}
int styleNum = style.getId();
if (!hashMap.containsKey(styleNum)) {
hashMap.put(styleNum, count++);
}
sb.append(String.format("%02d ", hashMap.get(styleNum)));
}
});
return sb.toString();
} finally {
myLock.unlock();
}
}
/**
* Returns terminal lines. Negative indexes are for history buffer. Non-negative for screen buffer.
*
* @param index index of line
* @return history lines for {@literal index<0}, screen line for index>=0
*/
public TerminalLine getLine(int index) {
if (index >= 0) {
if (index >= getHeight()) {
LOG.error("Attempt to get line out of bounds: " + index + " >= " + getHeight());
return TerminalLine.createEmpty();
}
return myScreenBuffer.getLine(index);
} else {
if (index < -getHistoryLinesCount()) {
LOG.error("Attempt to get line out of bounds: " + index + " < " + -getHistoryLinesCount());
return TerminalLine.createEmpty();
}
return myHistoryBuffer.getLine(getHistoryLinesCount() + index);
}
}
public List<String> getScreen() {
myLock.lock();
List<String> lines = new ArrayList<>();
try {
for (int row = 0; row < myHeight; row++) {
StringBuilder line = new StringBuilder(myScreenBuffer.getLine(row).getText());
for (int i = line.length(); i < myWidth; i++) {
line.append(' ');
}
if (line.length() > myWidth) {
line.setLength(myWidth);
}
lines.add(line.toString());
}
return lines;
} finally {
myLock.unlock();
}
}
public void processScreenLines(final int yStart, final int yCount, final StyledTextConsumer consumer) {
myScreenBuffer.processLines(yStart, yCount, consumer);
}
public void lock() {
myLock.lock();
}
public void unlock() {
myLock.unlock();
}
public boolean tryLock() {
return myLock.tryLock();
}
public int getWidth() {
return myWidth;
}
public int getHeight() {
return myHeight;
}
public int getHistoryLinesCount() {
return myHistoryBuffer.getLineCount();
}
public int getScreenLinesCount() {
return myScreenBuffer.getLineCount();
}
public char getBuffersCharAt(int x, int y) {
return getLine(y).charAt(x);
}
public TextStyle getStyleAt(int x, int y) {
return getLine(y).getStyleAt(x);
}
public Pair<Character, TextStyle> getStyledCharAt(int x, int y) {
synchronized (myScreenBuffer) {
TerminalLine line = getLine(y);
return new Pair<Character, TextStyle>(line.charAt(x), line.getStyleAt(x));
}
}
public char getCharAt(int x, int y) {
synchronized (myScreenBuffer) {
TerminalLine line = getLine(y);
return line.charAt(x);
}
}
public boolean isUsingAlternateBuffer() {
return myUsingAlternateBuffer;
}
public void useAlternateBuffer(boolean enabled) {
// myAlternateBuffer = enabled;
if (enabled) {
if (!myUsingAlternateBuffer) {
myScreenBufferBackup = myScreenBuffer;
myHistoryBufferBackup = myHistoryBuffer;
myScreenBuffer = createScreenBuffer();
myHistoryBuffer = createHistoryBuffer();
myUsingAlternateBuffer = true;
}
} else {
if (myUsingAlternateBuffer) {
myScreenBuffer = myScreenBufferBackup;
myHistoryBuffer = myHistoryBufferBackup;
myScreenBufferBackup = createScreenBuffer();
myHistoryBufferBackup = createHistoryBuffer();
myUsingAlternateBuffer = false;
}
}
fireModelChangeEvent();
}
public LinesBuffer getHistoryBuffer() {
return myHistoryBuffer;
}
public void insertLines(int y, int count, int scrollRegionBottom) {
myScreenBuffer.insertLines(y, count, scrollRegionBottom - 1, createFillerEntry());
fireModelChangeEvent();
}
// returns deleted lines
public LinesBuffer deleteLines(int y, int count, int scrollRegionBottom) {
LinesBuffer linesBuffer = myScreenBuffer.deleteLines(y, count, scrollRegionBottom - 1, createFillerEntry());
fireModelChangeEvent();
return linesBuffer;
}
public void clearLines(int startRow, int endRow) {
myScreenBuffer.clearLines(startRow, endRow, createFillerEntry());
fireModelChangeEvent();
}
public void eraseCharacters(int leftX, int rightX, int y) {
TextStyle style = createEmptyStyleWithCurrentColor();
if (y >= 0) {
myScreenBuffer.clearArea(leftX, y, rightX, y + 1, style);
fireModelChangeEvent();
} else {
LOG.error("Attempt to erase characters in line: " + y);
}
}
public void clearAll() {
myScreenBuffer.clearAll();
fireModelChangeEvent();
}
/**
* @param scrollOrigin row where a scrolling window starts, should be in the range [-history_lines_count, 0]
*/
public void processHistoryAndScreenLines(int scrollOrigin, int maximalLinesToProcess, StyledTextConsumer consumer) {
if (maximalLinesToProcess<0) {
//Process all lines in this case
maximalLinesToProcess = myHistoryBuffer.getLineCount() + myScreenBuffer.getLineCount();
}
int linesFromHistory = Math.min(-scrollOrigin, maximalLinesToProcess);
int y = myHistoryBuffer.getLineCount() + scrollOrigin;
if (y < 0) { // it seems that lower bound of scrolling can get out of sync with history buffer lines count
y = 0; // to avoid exception we start with the first line in this case
}
myHistoryBuffer.processLines(y, linesFromHistory, consumer, y);
if (linesFromHistory < maximalLinesToProcess) {
// we can show lines from screen buffer
myScreenBuffer.processLines(0, maximalLinesToProcess - linesFromHistory, consumer, -linesFromHistory);
}
}
public void clearHistory() {
myHistoryBuffer.clearAll();
fireModelChangeEvent();
}
void moveScreenLinesToHistory() {
myLock.lock();
try {
myScreenBuffer.removeBottomEmptyLines(myScreenBuffer.getLineCount() - 1, myScreenBuffer.getLineCount());
myScreenBuffer.moveTopLinesTo(myScreenBuffer.getLineCount(), myHistoryBuffer);
if (myHistoryBuffer.getLineCount() > 0) {
myHistoryBuffer.getLine(myHistoryBuffer.getLineCount() - 1).setWrapped(false);
}
}
finally {
myLock.unlock();
}
}
LinesBuffer getHistoryBufferOrBackup() {
return myUsingAlternateBuffer ? myHistoryBufferBackup : myHistoryBuffer;
}
LinesBuffer getScreenBufferOrBackup() {
return myUsingAlternateBuffer ? myScreenBufferBackup : myScreenBuffer;
}
public int findScreenLineIndex( TerminalLine line) {
return myScreenBuffer.findLineIndex(line);
}
public void clearTypeAheadPredictions() {
myScreenBuffer.clearTypeAheadPredictions();
myHistoryBuffer.clearTypeAheadPredictions();
fireModelChangeEvent();
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2022 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.test.jediterm.terminal.model;
import java.util.concurrent.TimeUnit;
import org.springframework.shell.test.jediterm.terminal.TextStyle;
/**
* @author jediterm authors
*/
public final class TerminalTypeAheadSettings {
public static final TerminalTypeAheadSettings DEFAULT = new TerminalTypeAheadSettings(
true,
TimeUnit.MILLISECONDS.toNanos(100),
new TextStyle(null)
);
private final boolean myEnabled;
private final long myLatencyThreshold;
private final TextStyle myTypeAheadStyle;
public TerminalTypeAheadSettings(boolean enabled, long latencyThreshold, TextStyle typeAheadColor) {
myEnabled = enabled;
myLatencyThreshold = latencyThreshold;
myTypeAheadStyle = typeAheadColor;
}
public boolean isEnabled() {
return myEnabled;
}
public long getLatencyThreshold() {
return myLatencyThreshold;
}
public TextStyle getTypeAheadStyle() {
return myTypeAheadStyle;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2022 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.test.jediterm.terminal.ui;
/**
* @author jediterm authors
*/
final class Cell {
private final int myLine;
private final int myColumn;
public Cell(int line, int column) {
myLine = line;
myColumn = column;
}
public int getLine() {
return myLine;
}
public int getColumn() {
return myColumn;
}
}

View File

@@ -0,0 +1,186 @@
/*
* Copyright 2022 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.test.jediterm.terminal.ui;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.shell.test.jediterm.terminal.Terminal;
import org.springframework.shell.test.jediterm.terminal.TerminalDisplay;
import org.springframework.shell.test.jediterm.terminal.TerminalStarter;
import org.springframework.shell.test.jediterm.terminal.TextStyle;
import org.springframework.shell.test.jediterm.terminal.TtyBasedArrayDataStream;
import org.springframework.shell.test.jediterm.terminal.TtyConnector;
import org.springframework.shell.test.jediterm.terminal.model.JediTerminal;
import org.springframework.shell.test.jediterm.terminal.model.LinesBuffer;
import org.springframework.shell.test.jediterm.terminal.model.StyleState;
import org.springframework.shell.test.jediterm.terminal.model.TerminalTextBuffer;
/**
* @author jediterm authors
*/
public class JediTermWidget implements TerminalSession, TerminalWidget {
private static final Logger log = LoggerFactory.getLogger(JediTermWidget.class);
private final TerminalPanel terminalPanel;
private final JediTerminal terminal;
private final AtomicBoolean sessionRunning = new AtomicBoolean();
private TtyConnector ttyConnector;
private TerminalStarter terminalStarter;
private Thread emuThread;
public JediTermWidget() {
this(80, 24);
}
public JediTermWidget(int columns, int lines) {
StyleState styleState = createDefaultStyle();
TerminalTextBuffer terminalTextBuffer = new TerminalTextBuffer(columns, lines, styleState,
LinesBuffer.DEFAULT_MAX_LINES_COUNT);
terminalPanel = createTerminalPanel(styleState, terminalTextBuffer);
terminal = new JediTerminal(terminalPanel, terminalTextBuffer, styleState);
terminalPanel.setCoordAccessor(terminal);
sessionRunning.set(false);
}
protected StyleState createDefaultStyle() {
StyleState styleState = new StyleState();
styleState.setDefaultStyle(new TextStyle());
return styleState;
}
protected TerminalPanel createTerminalPanel(StyleState styleState, TerminalTextBuffer terminalTextBuffer) {
return new TerminalPanel(terminalTextBuffer, styleState);
}
public TerminalDisplay getTerminalDisplay() {
return getTerminalPanel();
}
public TerminalPanel getTerminalPanel() {
return terminalPanel;
}
public void setTtyConnector(TtyConnector ttyConnector) {
this.ttyConnector = ttyConnector;
terminalStarter = createTerminalStarter(terminal, ttyConnector);
terminalPanel.setTerminalStarter(terminalStarter);
}
protected TerminalStarter createTerminalStarter(JediTerminal terminal, TtyConnector connector) {
TtyBasedArrayDataStream ttyBasedArrayDataStream = new TtyBasedArrayDataStream(connector);
return new TerminalStarter(terminal, connector, ttyBasedArrayDataStream);
}
@Override
public TtyConnector getTtyConnector() {
return ttyConnector;
}
@Override
public Terminal getTerminal() {
return terminal;
}
@Override
public String getSessionName() {
if (ttyConnector != null) {
return ttyConnector.getName();
} else {
return "Session";
}
}
public void start() {
if (!sessionRunning.get()) {
emuThread = new Thread(new EmulatorTask());
emuThread.start();
} else {
log.error("Should not try to start session again at this point... ");
}
}
public void stop() {
if (sessionRunning.get() && emuThread != null) {
emuThread.interrupt();
}
}
public boolean isSessionRunning() {
return sessionRunning.get();
}
@Override
public TerminalTextBuffer getTerminalTextBuffer() {
return terminalPanel.getTerminalTextBuffer();
}
public boolean canOpenSession() {
return !isSessionRunning();
}
@Override
public void setTerminalPanelListener(TerminalPanelListener terminalPanelListener) {
terminalPanel.setTerminalPanelListener(terminalPanelListener);
}
@Override
public TerminalSession getCurrentSession() {
return this;
}
@Override
public JediTermWidget createTerminalSession(TtyConnector ttyConnector) {
setTtyConnector(ttyConnector);
return this;
}
@Override
public void close() {
stop();
if (terminalStarter != null) {
terminalStarter.close();
}
terminalPanel.dispose();
}
class EmulatorTask implements Runnable {
public void run() {
try {
sessionRunning.set(true);
Thread.currentThread().setName("Connector-" + ttyConnector.getName());
if (ttyConnector.init()) {
terminalStarter.start();
}
} catch (Exception e) {
log.error("Exception running terminal", e);
} finally {
try {
ttyConnector.close();
} catch (Exception e) {
}
sessionRunning.set(false);
}
}
}
@Override
public TerminalStarter getTerminalStarter() {
return terminalStarter;
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2022 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.test.jediterm.terminal.ui;
/**
* @author jediterm authors
*/
final class LineCellInterval {
private final int myLine;
private final int myStartColumn;
private final int myEndColumn;
public LineCellInterval(int line, int startColumn, int endColumn) {
myLine = line;
myStartColumn = startColumn;
myEndColumn = endColumn;
}
public int getLine() {
return myLine;
}
public int getStartColumn() {
return myStartColumn;
}
public int getEndColumn() {
return myEndColumn;
}
public int getCellCount() {
return myEndColumn - myStartColumn + 1;
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2022 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.test.jediterm.terminal.ui;
/**
* @author jediterm authors
*/
public interface TerminalCoordinates {
int getX();
void setX(int x);
int getY();
void setY(int y);
}

View File

@@ -0,0 +1,185 @@
/*
* Copyright 2022 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.test.jediterm.terminal.ui;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.shell.test.jediterm.terminal.TerminalDisplay;
import org.springframework.shell.test.jediterm.terminal.TerminalOutputStream;
import org.springframework.shell.test.jediterm.terminal.TerminalStarter;
import org.springframework.shell.test.jediterm.terminal.model.LinesBuffer;
import org.springframework.shell.test.jediterm.terminal.model.StyleState;
import org.springframework.shell.test.jediterm.terminal.model.TerminalLine;
import org.springframework.shell.test.jediterm.terminal.model.TerminalTextBuffer;
/**
* @author jediterm authors
*/
public class TerminalPanel implements TerminalDisplay {
public static final double SCROLL_SPEED = 0.05;
protected int myCharSizeWidth = 0;
protected int myCharSizeHeight = 0;
protected int myTermSizeWidth = 80;
protected int myTermSizeHeight = 24;
private TerminalStarter myTerminalStarter = null;
// private TerminalSelection mySelection = null;
private TerminalPanelListener myTerminalPanelListener;
private final TerminalTextBuffer myTerminalTextBuffer;
protected int myClientScrollOrigin;
private String myWindowTitle = "Terminal";
private AtomicInteger scrollDy = new AtomicInteger(0);
private TerminalCoordinates myCoordsAccessor;
public TerminalPanel(TerminalTextBuffer terminalTextBuffer, StyleState styleState) {
myTerminalTextBuffer = terminalTextBuffer;
myTermSizeWidth = terminalTextBuffer.getWidth();
myTermSizeHeight = terminalTextBuffer.getHeight();
}
public TerminalPanelListener getTerminalPanelListener() {
return myTerminalPanelListener;
}
public void setCoordAccessor(TerminalCoordinates coordAccessor) {
myCoordsAccessor = coordAccessor;
}
public void setTerminalStarter(final TerminalStarter terminalStarter) {
myTerminalStarter = terminalStarter;
}
public void setTerminalPanelListener(final TerminalPanelListener terminalPanelListener) {
myTerminalPanelListener = terminalPanelListener;
}
public int getPixelWidth() {
return myCharSizeWidth * myTermSizeWidth + getInsetX();
}
public int getPixelHeight() {
return myCharSizeHeight * myTermSizeHeight;
}
public int getColumnCount() {
return myTermSizeWidth;
}
public int getRowCount() {
return myTermSizeHeight;
}
public String getWindowTitle() {
return myWindowTitle;
}
protected int getInsetX() {
return 4;
}
public enum TerminalCursorState {
SHOWING, HIDDEN, NO_FOCUS;
}
// Called in a background thread with myTerminalTextBuffer.lock() acquired
public void scrollArea(final int scrollRegionTop, final int scrollRegionSize, int dy) {
scrollDy.addAndGet(dy);
// mySelection = null;
}
public void beep() {
// if (mySettingsProvider.audibleBell()) {
// Toolkit.getDefaultToolkit().beep();
// }
}
public TerminalTextBuffer getTerminalTextBuffer() {
return myTerminalTextBuffer;
}
// public TerminalSelection getSelection() {
// return mySelection;
// }
@Override
public boolean ambiguousCharsAreDoubleWidth() {
return false;
}
@Override
public void setBracketedPasteMode(boolean enabled) {
// myBracketedPasteMode = enabled;
}
public LinesBuffer getScrollBuffer() {
return myTerminalTextBuffer.getHistoryBuffer();
}
public TerminalOutputStream getTerminalOutputStream() {
return myTerminalStarter;
}
@Override
public void setWindowTitle(String name) {
myWindowTitle = name;
if (myTerminalPanelListener != null) {
myTerminalPanelListener.onTitleChanged(myWindowTitle);
}
}
public void clearBuffer() {
clearBuffer(true);
}
/**
* @param keepLastLine true to keep last line (e.g. to keep terminal prompt)
* false to clear entire terminal panel (relevant for terminal console)
*/
protected void clearBuffer(boolean keepLastLine) {
if (!myTerminalTextBuffer.isUsingAlternateBuffer()) {
myTerminalTextBuffer.clearHistory();
if (myCoordsAccessor != null) {
if (keepLastLine) {
if (myCoordsAccessor.getY() > 0) {
TerminalLine lastLine = myTerminalTextBuffer.getLine(myCoordsAccessor.getY() - 1);
myTerminalTextBuffer.clearAll();
myCoordsAccessor.setY(0);
// myCursor.setY(1);
myTerminalTextBuffer.addLine(lastLine);
}
}
else {
myTerminalTextBuffer.clearAll();
myCoordsAccessor.setX(0);
myCoordsAccessor.setY(1);
// myCursor.setX(0);
// myCursor.setY(1);
}
}
// myBoundedRangeModel.setValue(0);
// updateScrolling(true);
// myClientScrollOrigin = myBoundedRangeModel.getValue();
}
}
public void dispose() {
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2022 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.test.jediterm.terminal.ui;
import org.springframework.shell.test.jediterm.terminal.RequestOrigin;
/**
* @author jediterm authors
*/
public interface TerminalPanelListener {
void onPanelResize(RequestOrigin origin);
void onTitleChanged(String title);
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2022 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.test.jediterm.terminal.ui;
import org.springframework.shell.test.jediterm.terminal.Terminal;
import org.springframework.shell.test.jediterm.terminal.TerminalStarter;
import org.springframework.shell.test.jediterm.terminal.TtyConnector;
import org.springframework.shell.test.jediterm.terminal.model.TerminalTextBuffer;
/**
* @author jediterm authors
*/
public interface TerminalSession {
void start();
TerminalTextBuffer getTerminalTextBuffer();
TerminalStarter getTerminalStarter();
Terminal getTerminal();
TtyConnector getTtyConnector();
String getSessionName();
void close();
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2022 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.test.jediterm.terminal.ui;
import org.springframework.shell.test.jediterm.terminal.TerminalDisplay;
import org.springframework.shell.test.jediterm.terminal.TtyConnector;
/**
* @author jediterm authors
*/
public interface TerminalWidget {
JediTermWidget createTerminalSession(TtyConnector ttyConnector);
boolean canOpenSession();
void setTerminalPanelListener(TerminalPanelListener terminalPanelListener);
TerminalSession getCurrentSession();
TerminalDisplay getTerminalDisplay();
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2022 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.test.jediterm.terminal.ui;
/**
* @author jediterm authors
*/
public class UIUtil {
public static final String OS_NAME = System.getProperty("os.name");
public static final String OS_VERSION = System.getProperty("os.version").toLowerCase();
protected static final String _OS_NAME = OS_NAME.toLowerCase();
public static final boolean isWindows = _OS_NAME.startsWith("windows");
public static final boolean isOS2 = _OS_NAME.startsWith("os/2") || _OS_NAME.startsWith("os2");
public static final boolean isMac = _OS_NAME.startsWith("mac");
public static final boolean isLinux = _OS_NAME.startsWith("linux");
public static final boolean isUnix = !isWindows && !isOS2;
public static final String JAVA_RUNTIME_VERSION = System.getProperty("java.runtime.version");
}

View File

@@ -0,0 +1,327 @@
/*
* Copyright 2022 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.test.jediterm.terminal.util;
import java.util.Arrays;
import org.springframework.shell.test.jediterm.terminal.emulator.charset.CharacterSets;
import org.springframework.shell.test.jediterm.terminal.model.CharBuffer;
import org.springframework.shell.test.jediterm.typeahead.Ascii;
/**
* @author jediterm authors
*/
public class CharUtils {
private static final int ESC = Ascii.ESC;
private static final int DEL = Ascii.DEL;
// NUL can only be at the end of the line
public static final char NUL_CHAR = 0x0;
public static final char EMPTY_CHAR = ' ';
//JediTerm Unicode private use area U+100000U+10FFFD
public static final char DWC = '\uE000'; //Second part of double-width character
private CharUtils() {
}
private static final String[] NONPRINTING_NAMES = {"NUL", "SOH", "STX", "ETX", "EOT", "ENQ",
"ACK", "BEL", "BS", "TAB", "LF", "VT", "FF", "CR", "S0", "S1",
"DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", "CAN",
"EM", "SUB", "ESC", "FS", "GS", "RS", "US"};
public static byte[] VT102_RESPONSE = makeCode(ESC, '[', '?', '6', 'c');
public static String getNonControlCharacters(int maxChars, char[] buf, int offset, int charsLength) {
int len = Math.min(maxChars, charsLength);
final int origLen = len;
char tmp;
while (len > 0) {
tmp = buf[offset++];
if (0x20 <= tmp) { //stop when we reach control chars
len--;
continue;
}
offset--;
break;
}
int length = origLen - len;
return new String(buf, offset - length, length);
}
public static int countDoubleWidthCharacters(char[] buf, int start, int length, boolean ambiguousIsDWC) {
int cnt = 0;
for (int i = 0; i < length; i++) {
int ucs = Character.codePointAt(buf, i + start);
if (isDoubleWidthCharacter(ucs, ambiguousIsDWC)) {
cnt++;
}
}
return cnt;
}
public enum CharacterType {
NONPRINTING,
PRINTING,
NONASCII, NONE
}
public static CharacterType appendChar(final StringBuilder sb, final CharacterType last, final char c) {
if (c <= 0x1F) {
sb.append(EMPTY_CHAR);
sb.append(NONPRINTING_NAMES[c]);
return CharacterType.NONPRINTING;
} else if (c == DEL) {
sb.append(" DEL");
return CharacterType.NONPRINTING;
} else if (c > 0x1F && c <= 0x7E) {
if (last != CharacterType.PRINTING) sb.append(EMPTY_CHAR);
sb.append(c);
return CharacterType.PRINTING;
} else {
sb.append(" 0x").append(Integer.toHexString(c));
return CharacterType.NONASCII;
}
}
public static void appendBuf(final StringBuilder sb, final char[] bs, final int begin, final int length) {
CharacterType last = CharacterType.NONPRINTING;
final int end = begin + length;
for (int i = begin; i < end; i++) {
final char c = (char) bs[i];
last = appendChar(sb, last, c);
}
}
public static byte[] makeCode(final int... bytesAsInt) {
final byte[] bytes = new byte[bytesAsInt.length];
int i = 0;
for (final int byteAsInt : bytesAsInt) {
bytes[i] = (byte) byteAsInt;
i++;
}
return bytes;
}
/**
* Computes text length as sum of characters length, treating double-width(full-width) characters as 2, normal-width(half-width) as 1
* (Read http://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms)
*/
public static int getTextLengthDoubleWidthAware(char[] buffer, int start, int length, boolean ambiguousIsDWC) {
int result = 0;
for (int i = start; i < start + length; i++) {
result += (buffer[i] != CharUtils.DWC) && isDoubleWidthCharacter(buffer[i], ambiguousIsDWC) && !((i + 1 < start + length) && (buffer[i + 1] == CharUtils.DWC)) ? 2 : 1;
}
return result;
}
public static boolean isDoubleWidthCharacter(int c, boolean ambiguousIsDWC) {
if (c == DWC || c <= 0xa0 || (c > 0x452 && c < 0x1100)) {
return false;
}
return mk_wcwidth(c, ambiguousIsDWC) == 2;
}
public static CharBuffer heavyDecCompatibleBuffer(CharBuffer buf) {
char[] c = Arrays.copyOfRange(buf.getBuf(), 0, buf.getBuf().length);
for (int i = 0; i < c.length; i++) {
c[i] = CharacterSets.getHeavyDecBoxChar(c[i]);
}
return new CharBuffer(c, buf.getStart(), buf.length());
}
// The following code and data in converted from the https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
// which can be treated a standard way to determine the width of a character
private static final char[][] COMBINING = new char[][]{
new char[]{0x0300, 0x036F}, new char[]{0x0483, 0x0486}, new char[]{0x0488, 0x0489},
new char[]{0x0591, 0x05BD}, new char[]{0x05BF, 0x05BF}, new char[]{0x05C1, 0x05C2},
new char[]{0x05C4, 0x05C5}, new char[]{0x05C7, 0x05C7}, new char[]{0x0600, 0x0603},
new char[]{0x0610, 0x0615}, new char[]{0x064B, 0x065E}, new char[]{0x0670, 0x0670},
new char[]{0x06D6, 0x06E4}, new char[]{0x06E7, 0x06E8}, new char[]{0x06EA, 0x06ED},
new char[]{0x070F, 0x070F}, new char[]{0x0711, 0x0711}, new char[]{0x0730, 0x074A},
new char[]{0x07A6, 0x07B0}, new char[]{0x07EB, 0x07F3}, new char[]{0x0901, 0x0902},
new char[]{0x093C, 0x093C}, new char[]{0x0941, 0x0948}, new char[]{0x094D, 0x094D},
new char[]{0x0951, 0x0954}, new char[]{0x0962, 0x0963}, new char[]{0x0981, 0x0981},
new char[]{0x09BC, 0x09BC}, new char[]{0x09C1, 0x09C4}, new char[]{0x09CD, 0x09CD},
new char[]{0x09E2, 0x09E3}, new char[]{0x0A01, 0x0A02}, new char[]{0x0A3C, 0x0A3C},
new char[]{0x0A41, 0x0A42}, new char[]{0x0A47, 0x0A48}, new char[]{0x0A4B, 0x0A4D},
new char[]{0x0A70, 0x0A71}, new char[]{0x0A81, 0x0A82}, new char[]{0x0ABC, 0x0ABC},
new char[]{0x0AC1, 0x0AC5}, new char[]{0x0AC7, 0x0AC8}, new char[]{0x0ACD, 0x0ACD},
new char[]{0x0AE2, 0x0AE3}, new char[]{0x0B01, 0x0B01}, new char[]{0x0B3C, 0x0B3C},
new char[]{0x0B3F, 0x0B3F}, new char[]{0x0B41, 0x0B43}, new char[]{0x0B4D, 0x0B4D},
new char[]{0x0B56, 0x0B56}, new char[]{0x0B82, 0x0B82}, new char[]{0x0BC0, 0x0BC0},
new char[]{0x0BCD, 0x0BCD}, new char[]{0x0C3E, 0x0C40}, new char[]{0x0C46, 0x0C48},
new char[]{0x0C4A, 0x0C4D}, new char[]{0x0C55, 0x0C56}, new char[]{0x0CBC, 0x0CBC},
new char[]{0x0CBF, 0x0CBF}, new char[]{0x0CC6, 0x0CC6}, new char[]{0x0CCC, 0x0CCD},
new char[]{0x0CE2, 0x0CE3}, new char[]{0x0D41, 0x0D43}, new char[]{0x0D4D, 0x0D4D},
new char[]{0x0DCA, 0x0DCA}, new char[]{0x0DD2, 0x0DD4}, new char[]{0x0DD6, 0x0DD6},
new char[]{0x0E31, 0x0E31}, new char[]{0x0E34, 0x0E3A}, new char[]{0x0E47, 0x0E4E},
new char[]{0x0EB1, 0x0EB1}, new char[]{0x0EB4, 0x0EB9}, new char[]{0x0EBB, 0x0EBC},
new char[]{0x0EC8, 0x0ECD}, new char[]{0x0F18, 0x0F19}, new char[]{0x0F35, 0x0F35},
new char[]{0x0F37, 0x0F37}, new char[]{0x0F39, 0x0F39}, new char[]{0x0F71, 0x0F7E},
new char[]{0x0F80, 0x0F84}, new char[]{0x0F86, 0x0F87}, new char[]{0x0F90, 0x0F97},
new char[]{0x0F99, 0x0FBC}, new char[]{0x0FC6, 0x0FC6}, new char[]{0x102D, 0x1030},
new char[]{0x1032, 0x1032}, new char[]{0x1036, 0x1037}, new char[]{0x1039, 0x1039},
new char[]{0x1058, 0x1059}, new char[]{0x1160, 0x11FF}, new char[]{0x135F, 0x135F},
new char[]{0x1712, 0x1714}, new char[]{0x1732, 0x1734}, new char[]{0x1752, 0x1753},
new char[]{0x1772, 0x1773}, new char[]{0x17B4, 0x17B5}, new char[]{0x17B7, 0x17BD},
new char[]{0x17C6, 0x17C6}, new char[]{0x17C9, 0x17D3}, new char[]{0x17DD, 0x17DD},
new char[]{0x180B, 0x180D}, new char[]{0x18A9, 0x18A9}, new char[]{0x1920, 0x1922},
new char[]{0x1927, 0x1928}, new char[]{0x1932, 0x1932}, new char[]{0x1939, 0x193B},
new char[]{0x1A17, 0x1A18}, new char[]{0x1B00, 0x1B03}, new char[]{0x1B34, 0x1B34},
new char[]{0x1B36, 0x1B3A}, new char[]{0x1B3C, 0x1B3C}, new char[]{0x1B42, 0x1B42},
new char[]{0x1B6B, 0x1B73}, new char[]{0x1DC0, 0x1DCA}, new char[]{0x1DFE, 0x1DFF},
new char[]{0x200B, 0x200F}, new char[]{0x202A, 0x202E}, new char[]{0x2060, 0x2063},
new char[]{0x206A, 0x206F}, new char[]{0x20D0, 0x20EF}, new char[]{0x302A, 0x302F},
new char[]{0x3099, 0x309A}, new char[]{0xA806, 0xA806}, new char[]{0xA80B, 0xA80B},
new char[]{0xA825, 0xA826}, new char[]{0xFB1E, 0xFB1E}, new char[]{0xFE00, 0xFE0F},
new char[]{0xFE20, 0xFE23}, new char[]{0xFEFF, 0xFEFF}, new char[]{0xFFF9, 0xFFFB}
};
private static final char[][] AMBIGUOUS = new char[][]{
new char[]{0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8},
new char[]{0x00AA, 0x00AA}, new char[]{0x00AE, 0x00AE}, new char[]{0x00B0, 0x00B4},
new char[]{0x00B6, 0x00BA}, new char[]{0x00BC, 0x00BF}, new char[]{0x00C6, 0x00C6},
new char[]{0x00D0, 0x00D0}, new char[]{0x00D7, 0x00D8}, new char[]{0x00DE, 0x00E1},
new char[]{0x00E6, 0x00E6}, new char[]{0x00E8, 0x00EA}, new char[]{0x00EC, 0x00ED},
new char[]{0x00F0, 0x00F0}, new char[]{0x00F2, 0x00F3}, new char[]{0x00F7, 0x00FA},
new char[]{0x00FC, 0x00FC}, new char[]{0x00FE, 0x00FE}, new char[]{0x0101, 0x0101},
new char[]{0x0111, 0x0111}, new char[]{0x0113, 0x0113}, new char[]{0x011B, 0x011B},
new char[]{0x0126, 0x0127}, new char[]{0x012B, 0x012B}, new char[]{0x0131, 0x0133},
new char[]{0x0138, 0x0138}, new char[]{0x013F, 0x0142}, new char[]{0x0144, 0x0144},
new char[]{0x0148, 0x014B}, new char[]{0x014D, 0x014D}, new char[]{0x0152, 0x0153},
new char[]{0x0166, 0x0167}, new char[]{0x016B, 0x016B}, new char[]{0x01CE, 0x01CE},
new char[]{0x01D0, 0x01D0}, new char[]{0x01D2, 0x01D2}, new char[]{0x01D4, 0x01D4},
new char[]{0x01D6, 0x01D6}, new char[]{0x01D8, 0x01D8}, new char[]{0x01DA, 0x01DA},
new char[]{0x01DC, 0x01DC}, new char[]{0x0251, 0x0251}, new char[]{0x0261, 0x0261},
new char[]{0x02C4, 0x02C4}, new char[]{0x02C7, 0x02C7}, new char[]{0x02C9, 0x02CB},
new char[]{0x02CD, 0x02CD}, new char[]{0x02D0, 0x02D0}, new char[]{0x02D8, 0x02DB},
new char[]{0x02DD, 0x02DD}, new char[]{0x02DF, 0x02DF}, new char[]{0x0391, 0x03A1},
new char[]{0x03A3, 0x03A9}, new char[]{0x03B1, 0x03C1}, new char[]{0x03C3, 0x03C9},
new char[]{0x0401, 0x0401}, new char[]{0x0410, 0x044F}, new char[]{0x0451, 0x0451},
new char[]{0x2010, 0x2010}, new char[]{0x2013, 0x2016}, new char[]{0x2018, 0x2019},
new char[]{0x201C, 0x201D}, new char[]{0x2020, 0x2022}, new char[]{0x2024, 0x2027},
new char[]{0x2030, 0x2030}, new char[]{0x2032, 0x2033}, new char[]{0x2035, 0x2035},
new char[]{0x203B, 0x203B}, new char[]{0x203E, 0x203E}, new char[]{0x2074, 0x2074},
new char[]{0x207F, 0x207F}, new char[]{0x2081, 0x2084}, new char[]{0x20AC, 0x20AC},
new char[]{0x2103, 0x2103}, new char[]{0x2105, 0x2105}, new char[]{0x2109, 0x2109},
new char[]{0x2113, 0x2113}, new char[]{0x2116, 0x2116}, new char[]{0x2121, 0x2122},
new char[]{0x2126, 0x2126}, new char[]{0x212B, 0x212B}, new char[]{0x2153, 0x2154},
new char[]{0x215B, 0x215E}, new char[]{0x2160, 0x216B}, new char[]{0x2170, 0x2179},
new char[]{0x2190, 0x2199}, new char[]{0x21B8, 0x21B9}, new char[]{0x21D2, 0x21D2},
new char[]{0x21D4, 0x21D4}, new char[]{0x21E7, 0x21E7}, new char[]{0x2200, 0x2200},
new char[]{0x2202, 0x2203}, new char[]{0x2207, 0x2208}, new char[]{0x220B, 0x220B},
new char[]{0x220F, 0x220F}, new char[]{0x2211, 0x2211}, new char[]{0x2215, 0x2215},
new char[]{0x221A, 0x221A}, new char[]{0x221D, 0x2220}, new char[]{0x2223, 0x2223},
new char[]{0x2225, 0x2225}, new char[]{0x2227, 0x222C}, new char[]{0x222E, 0x222E},
new char[]{0x2234, 0x2237}, new char[]{0x223C, 0x223D}, new char[]{0x2248, 0x2248},
new char[]{0x224C, 0x224C}, new char[]{0x2252, 0x2252}, new char[]{0x2260, 0x2261},
new char[]{0x2264, 0x2267}, new char[]{0x226A, 0x226B}, new char[]{0x226E, 0x226F},
new char[]{0x2282, 0x2283}, new char[]{0x2286, 0x2287}, new char[]{0x2295, 0x2295},
new char[]{0x2299, 0x2299}, new char[]{0x22A5, 0x22A5}, new char[]{0x22BF, 0x22BF},
new char[]{0x2312, 0x2312}, new char[]{0x2460, 0x24E9}, new char[]{0x24EB, 0x254B},
new char[]{0x2550, 0x2573}, new char[]{0x2580, 0x258F}, new char[]{0x2592, 0x2595},
new char[]{0x25A0, 0x25A1}, new char[]{0x25A3, 0x25A9}, new char[]{0x25B2, 0x25B3},
new char[]{0x25B6, 0x25B7}, new char[]{0x25BC, 0x25BD}, new char[]{0x25C0, 0x25C1},
new char[]{0x25C6, 0x25C8}, new char[]{0x25CB, 0x25CB}, new char[]{0x25CE, 0x25D1},
new char[]{0x25E2, 0x25E5}, new char[]{0x25EF, 0x25EF}, new char[]{0x2605, 0x2606},
new char[]{0x2609, 0x2609}, new char[]{0x260E, 0x260F}, new char[]{0x2614, 0x2615},
new char[]{0x261C, 0x261C}, new char[]{0x261E, 0x261E}, new char[]{0x2640, 0x2640},
new char[]{0x2642, 0x2642}, new char[]{0x2660, 0x2661}, new char[]{0x2663, 0x2665},
new char[]{0x2667, 0x266A}, new char[]{0x266C, 0x266D}, new char[]{0x266F, 0x266F},
new char[]{0x273D, 0x273D}, new char[]{0x2776, 0x277F}, new char[]{0xE000, 0xF8FF},
new char[]{0xFFFD, 0xFFFD}};
/* auxiliary function for binary search in interval table */
static int bisearch(char ucs, char[][] table, int max) {
int min = 0;
int mid;
if (ucs < table[0][0] || ucs > table[max][1])
return 0;
while (max >= min) {
mid = (min + max) / 2;
if (ucs > table[mid][1])
min = mid + 1;
else if (ucs < table[mid][0])
max = mid - 1;
else
return 1;
}
return 0;
}
private static int mk_wcwidth(int ucs, boolean ambiguousIsDoubleWidth) {
/* sorted list of non-overlapping intervals of non-spacing characters */
/* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */
/* test for8-bnew char[]it control characters */
if (ucs == 0)
return 0;
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
return -1;
if (ambiguousIsDoubleWidth) {
if (bisearch((char)ucs, AMBIGUOUS, AMBIGUOUS.length-1) > 0) {
return 2;
}
}
/* binary search in table of non-spacing characters */
if (bisearch((char)ucs, COMBINING, COMBINING.length-1) > 0) {
return 0;
}
/* if we arrive here, ucs is not a combining or C0/C1 control character */
return 1 +
((ucs >= 0x1100 &&
(ucs <= 0x115f || /* Hangul Jamo init. consonants */
ucs == 0x2329 || ucs == 0x232a ||
(ucs >= 0x2e80 && ucs <= 0xa4cf &&
ucs != 0x303f) || /* CJK ... Yi */
(ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */
(ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */
(ucs >= 0xfe10 && ucs <= 0xfe19) || /* Vertical forms */
(ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */
(ucs >= 0xff00 && ucs <= 0xff60) || /* Fullwidth Forms */
(ucs >= 0xffe0 && ucs <= 0xffe6) ||
(ucs >= 0x20000 && ucs <= 0x2fffd) ||
(ucs >= 0x30000 && ucs <= 0x3fffd))) ? 1 : 0);
}
public static String toHumanReadableText(String escapeSequence) {
return escapeSequence.replace("\u001b", "ESC")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\u0007", "BEL")
.replace(" ", "<S>")
.replace("\b", "\\b");
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2022 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.test.jediterm.terminal.util;
/**
* @author jediterm authors
*/
public class Pair<A, B> {
public final A first;
public final B second;
public static <A, B> Pair<A, B> create(A first, B second) {
return new Pair<A, B>(first, second);
}
public static <T> T getFirst(Pair<T, ?> pair) {
return pair != null ? pair.first : null;
}
public static <T> T getSecond(Pair<?, T> pair) {
return pair != null ? pair.second : null;
}
public static <A, B> Pair<A, B> empty() {
return create(null, null);
}
public Pair(A first, B second) {
this.first = first;
this.second = second;
}
public final A getFirst() {
return first;
}
public final B getSecond() {
return second;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pair<?, ?> pair = (Pair<?, ?>)o;
if (first != null ? !first.equals(pair.first) : pair.first != null) return false;
if (second != null ? !second.equals(pair.second) : pair.second != null) return false;
return true;
}
public int hashCode() {
int result = first != null ? first.hashCode() : 0;
result = 31 * result + (second != null ? second.hashCode() : 0);
return result;
}
public String toString() {
return "<" + first + "," + second + ">";
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright 2022 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.test.jediterm.terminal.util;
import java.lang.reflect.Array;
import java.util.BitSet;
// In Java 5, the java.util.Arrays class has no copyOf() members...
/**
* @author jediterm authors
*/
public class Util {
@SuppressWarnings("unchecked")
public static <T> T[] copyOf(T[] original, int newLength) {
Class<T> type = (Class<T>)original.getClass().getComponentType();
T[] newArr = (T[])Array.newInstance(type, newLength);
System.arraycopy(original, 0, newArr, 0, Math.min(original.length, newLength));
return newArr;
}
public static int[] copyOf(int[] original, int newLength) {
int[] newArr = new int[newLength];
System.arraycopy(original, 0, newArr, 0, Math.min(original.length, newLength));
return newArr;
}
public static char[] copyOf(char[] original, int newLength) {
char[] newArr = new char[newLength];
System.arraycopy(original, 0, newArr, 0, Math.min(original.length, newLength));
return newArr;
}
public static void bitsetCopy(BitSet src, int srcOffset, BitSet dest, int destOffset, int length) {
for (int i = 0; i < length; i++) {
dest.set(destOffset + i, src.get(srcOffset + i));
}
}
public static String trimTrailing(String string) {
int index = string.length() - 1;
while (index >= 0 && Character.isWhitespace(string.charAt(index))) index--;
return string.substring(0, index + 1);
}
public static boolean containsIgnoreCase(String where, String what) {
return indexOfIgnoreCase(where, what, 0) >= 0;
}
/**
* Implementation copied from {@link String#indexOf(String, int)} except character comparisons made case insensitive
*/
public static int indexOfIgnoreCase(String where, String what, int fromIndex) {
int targetCount = what.length();
int sourceCount = where.length();
if (fromIndex >= sourceCount) {
return targetCount == 0 ? sourceCount : -1;
}
if (fromIndex < 0) {
fromIndex = 0;
}
if (targetCount == 0) {
return fromIndex;
}
char first = what.charAt(0);
int max = sourceCount - targetCount;
for (int i = fromIndex; i <= max; i++) {
/* Look for first character. */
if (!charsEqualIgnoreCase(where.charAt(i), first)) {
while (++i <= max && !charsEqualIgnoreCase(where.charAt(i), first)) ;
}
/* Found first character, now look at the rest of v2 */
if (i <= max) {
int j = i + 1;
int end = j + targetCount - 1;
for (int k = 1; j < end && charsEqualIgnoreCase(where.charAt(j), what.charAt(k)); j++, k++) ;
if (j == end) {
/* Found whole string. */
return i;
}
}
}
return -1;
}
public static boolean charsEqualIgnoreCase(char a, char b) {
return a == b || toUpperCase(a) == toUpperCase(b) || toLowerCase(a) == toLowerCase(b);
}
private static char toLowerCase(char b) {
return Character.toLowerCase(b);
}
private static char toUpperCase(char a) {
return Character.toUpperCase(a);
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2022 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.test.jediterm.typeahead;
public final class Ascii {
/**
* Null ('\0'): The all-zeros character which may serve to accomplish time fill and media fill.
* Normally used as a C string terminator.
*/
public static final byte NUL = 0;
/**
* Enquiry: A communication control character used in data communication systems as a request for
* a response from a remote station. It may be used as a "Who Are You" (WRU) to obtain
* identification, or may be used to obtain station status, or both.
*/
public static final byte ENQ = 5;
/**
* Bell ('\a'): A character for use when there is a need to call for human attention. It may
* control alarm or attention devices.
*/
public static final byte BEL = 7;
/**
* Backspace ('\b'): A format effector which controls the movement of the printing position one
* printing space backward on the same printing line. (Applicable also to display devices.)
*/
public static final byte BS = 8;
/**
* Horizontal Tabulation ('\t'): A format effector which controls the movement of the printing
* position to the next in a series of predetermined positions along the printing line.
* (Applicable also to display devices and the skip function on punched cards.)
*/
public static final byte HT = 9;
/**
* Line Feed ('\n'): A format effector which controls the movement of the printing position to the
* next printing line. (Applicable also to display devices.) Where appropriate, this character may
* have the meaning "New Line" (NL), a format effector which controls the movement of the printing
* point to the first printing position on the next printing line. Use of this convention requires
* agreement between sender and recipient of data.
*/
public static final byte LF = 10;
/**
* Vertical Tabulation ('\v'): A format effector which controls the movement of the printing
* position to the next in a series of predetermined printing lines. (Applicable also to display
* devices.)
*/
public static final byte VT = 11;
/**
* Form Feed ('\f'): A format effector which controls the movement of the printing position to the
* first pre-determined printing line on the next form or page. (Applicable also to display
* devices.)
*/
public static final byte FF = 12;
/**
* Carriage Return ('\r'): A format effector which controls the movement of the printing position
* to the first printing position on the same printing line. (Applicable also to display devices.)
*/
public static final byte CR = 13;
/**
* Shift Out: A control character indicating that the code combinations which follow shall be
* interpreted as outside of the character set of the standard code table until a Shift In
* character is reached.
*/
public static final byte SO = 14;
/**
* Shift In: A control character indicating that the code combinations which follow shall be
* interpreted according to the standard code table.
*/
public static final byte SI = 15;
/**
* Escape: A control character intended to provide code extension (supplementary characters) in
* general information interchange. The Escape character itself is a prefix affecting the
* interpretation of a limited number of contiguously following characters.
*/
public static final byte ESC = 27;
/**
* Unit Separator: These four information separators may be used within data in optional fashion,
* except that their hierarchical relationship shall be: FS is the most inclusive, then GS, then
* RS, and US is least inclusive. (The content and length of a File, Group, Record, or Unit are
* not specified.)
*/
public static final byte US = 31;
/**
* Delete: This character is used primarily to "erase" or "obliterate" erroneous or unwanted
* characters in perforated tape.
*/
public static final byte DEL = 127;
private Ascii() {}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2022 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.test;
public class ShellClientTests {
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2022 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.test;
import java.util.Arrays;
import java.util.List;
import org.assertj.core.api.AssertProvider;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
public class ShellScreenAssertTests {
List<String> LINES1 = Arrays.asList("line1", "line2");
@Test
void assertContainsTextShouldContain() {
assertThat(forScreen(ShellScreen.of(LINES1))).containsText("line1");
}
@Test
void assertContainsTextShouldThrow() {
assertThatExceptionOfType(AssertionError.class)
.isThrownBy(() -> assertThat(forScreen(ShellScreen.of(LINES1))).containsText("linex"));
}
private AssertProvider<ShellScreenAssert> forScreen(ShellScreen screen) {
return () -> new ShellScreenAssert(screen);
}
}