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:
@@ -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}"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
----
|
||||
====
|
||||
18
spring-shell-docs/src/main/asciidoc/using-shell-testing.adoc
Normal file
18
spring-shell-docs/src/main/asciidoc/using-shell-testing.adoc
Normal 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[]
|
||||
@@ -13,3 +13,5 @@ include::using-shell-components.adoc[]
|
||||
include::using-shell-customization.adoc[]
|
||||
|
||||
include::using-shell-execution.adoc[]
|
||||
|
||||
include::using-shell-testing.adoc[]
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#logging:
|
||||
# file:
|
||||
# name: xxx.log
|
||||
# level:
|
||||
# root: debug
|
||||
# org:
|
||||
# jline: debug
|
||||
# springframework:
|
||||
# shell: trace
|
||||
14
spring-shell-test/spring-shell-test.gradle
Normal file
14
spring-shell-test/spring-shell-test.gradle
Normal 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'
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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+100000–U+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");
|
||||
}
|
||||
}
|
||||
@@ -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 + ">";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user