Skip to content

feat: add support for named terminal add-ons #99

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.flowingcode.addons</groupId>
<artifactId>xterm-console</artifactId>
<version>3.1.1-SNAPSHOT</version>
<version>3.2.0-SNAPSHOT</version>
<name>XTerm Console Addon</name>
<description>Integration of xterm.js for Vaadin Flow</description>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*-
* #%L
* XTerm Console Addon
* %%
* Copyright (C) 2020 - 2025 Flowing Code
* %%
* 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
*
* http://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.
* #L%
*/
package com.flowingcode.vaadin.addons.xterm;

import com.vaadin.flow.dom.Element;
import com.vaadin.flow.internal.JsonCodec;
import elemental.json.Json;
import elemental.json.JsonArray;
import java.io.Serializable;

/**
* Represents an abstract base class for server-side terminal add-ons that have a corresponding
* client-side (JavaScript) component or require interaction with the client-side terminal
* environment. It extends {@link TerminalAddon} and specializes its use for client-aware
* operations.
*
* @author Javier Godoy / Flowing Code S.A.
*/
@SuppressWarnings("serial")
public abstract class ClientTerminalAddon extends TerminalAddon {

private final XTermBase xterm;

/**
* Constructs a new {@code ClientTerminalAddon} and associates it with the specified
* {@link XTermBase} instance.
* <p>
* This constructor ensures the add-on is registered with the terminal and verifies that the
* add-on's name, as returned by {@link #getName()}, is not {@code null}. A non-null name is
* required for client-side add-ons to be uniquely identified and targeted for JavaScript
* execution.
* </p>
*
* @param xterm the {@link XTermBase} instance this add-on will be attached to. Must not be
* {@code null}.
* @throws NullPointerException if {@code xterm} is {@code null}
* @throws IllegalStateException if {@link #getName()} returns {@code null} immediately after
* superclass construction. This check relies on {@code getName()} being a static value.
*/
protected ClientTerminalAddon(XTermBase xterm) {
super(xterm);
this.xterm = xterm;
if (getName() == null) {
throw new IllegalStateException("getName() must return a non-null value");
}
}

/**
* The xterm instance that this add-on is associated with.
*/
protected XTermBase getXterm() {
return xterm;
}

/**
* Retrieves the unique name of this client-side add-on.
* <p>
* This name is used by {@link #executeJs(String, Serializable...)} to target the corresponding
* JavaScript object on the client (i.e., {@code this.addons[name]} within the client-side
* terminal's scope). The name effectively acts as a key in a client-side add-ons collection
* managed by the terminal.
* </p>
*
* @return the unique, non-null string identifier for the client-side counterpart of this add-on.
* Subclasses must implement this to provide a name for add-on-specific JavaScript
* execution.
*/
protected abstract String getName();

/**
* Executes a JavaScript {@code expression} in the context of this add-on, with the specified
* {@code parameters}.
*
* @see #getName()
* @see Element#executeJs(String, Serializable...)
*/
protected final void executeJs(String expression, Serializable... parameters) {
String name = getName();

JsonArray args = Json.createArray();
for (int i = 0; i < parameters.length; i++) {
args.set(i, JsonCodec.encodeWithTypeInfo(parameters[i]));
}

expression = expression.replaceAll("\\$(\\d+)", "\\$1[$1]");
xterm.executeJs("(function(){" + expression + "}).apply(this.addons[$0],$1);", name, args);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* #%L
* XTerm Console Addon
* %%
* Copyright (C) 2020 - 2023 Flowing Code
* Copyright (C) 2020 - 2025 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -43,7 +43,9 @@
* addon.writePrompt();
* </pre>
*/
public class PreserveStateAddon implements ITerminal, ITerminalOptions {
public class PreserveStateAddon extends TerminalAddon
implements ITerminal, ITerminalOptions {

/**
* The xterm to delegate all calls to.
*/
Expand Down Expand Up @@ -80,6 +82,7 @@ public class PreserveStateAddon implements ITerminal, ITerminalOptions {
private final ITerminalOptions optionsDelegate;

public PreserveStateAddon(XTerm xterm) {
super(xterm);
this.xterm = Objects.requireNonNull(xterm);
optionsMemoizer = new StateMemoizer(xterm, ITerminalOptions.class);
optionsDelegate = (ITerminalOptions) optionsMemoizer.getProxy();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*-
* #%L
* XTerm Console Addon
* %%
* Copyright (C) 2020 - 2025 Flowing Code
* %%
* 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
*
* http://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.
* #L%
*/
package com.flowingcode.vaadin.addons.xterm;

import java.io.Serializable;
import java.util.Objects;

/**
* Represents an abstract base class for server-side add-ons designed to extend or modify the
* functionality of an {@link XTermBase} terminal instance.
* <p>
* Concrete add-on implementations should subclass this class to provide specific features. Each
* add-on is tightly coupled with a specific {@code XTermBase} instance, allowing it to interact
* with and enhance that terminal.
* </p>
*
* @author Javier Godoy / Flowing Code S.A.
*/
@SuppressWarnings("serial")
public abstract class TerminalAddon implements Serializable {

/**
* Constructs a new {@code TerminalAddon} and associates it with the provided {@link XTermBase}
* instance.
*
* @param xterm the {@code XTermBase} instance to which this add-on will be attached
* @throws NullPointerException if the provided {@code xterm} is {@code null}
*/
protected TerminalAddon(XTermBase xterm) {
Objects.requireNonNull(xterm);
xterm.registerServerSideAddon(this);
}

}
47 changes: 46 additions & 1 deletion src/main/java/com/flowingcode/vaadin/addons/xterm/XTermBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* #%L
* XTerm Console Addon
* %%
* Copyright (C) 2020 - 2023 Flowing Code
* Copyright (C) 2020 - 2025 Flowing Code
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -43,6 +43,7 @@
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
Expand All @@ -69,6 +70,8 @@ public abstract class XTermBase extends Component

private List<Command> deferredCommands;

private final List<TerminalAddon> addons = new ArrayList<>();

private class ProxyInvocationHandler implements InvocationHandler, Serializable {

@Override
Expand Down Expand Up @@ -281,4 +284,46 @@ private Registration addCustomKeyListener(
public void setEnabled(boolean enabled) {
HasEnabled.super.setEnabled(enabled);
}

/**
* Retrieves a registered server-side add-on instance of a specific type.
* <p>
* Example usage:
* </p>
*
* <pre>{@code
* MySpecificAddon addon = terminal.getAddon(MySpecificAddon.class);
* if (addon != null) {
* addon.doSomethingSpecific();
* }
* }</pre>
*
* @param <T> the type of the add-on to retrieve. This is inferred from the {@code clazz}
* parameter.
* @param clazz the {@code Class} object representing the type of the add-on to retrieve. Must not
* be {@code null}.
* @return the registered add-on instance that is of the specified {@code Class<T>}, or
* {@code null} if no such add-on is found
* @throws NullPointerException if {@code clazz} is {@code null}
*/
public <T extends TerminalAddon> T getAddon(Class<? extends T> clazz) {
return addons.stream().filter(clazz::isInstance).map(clazz::cast).findFirst().orElse(null);
}

/**
* Registers a server-side add-on with this terminal instance. This method is called by the add-on
* itself during its construction.
*
* @param addon the add-on to register. Must not be {@code null}.
* @throws NullPointerException if {@code addon} is {@code null}
* @throws IllegalStateException if an add-on of the same class as the provided {@code addon} is
* already registered with this terminal instance
*/
final <T extends TerminalAddon> void registerServerSideAddon(T addon) {
if (getAddon(addon.getClass()) != null) {
throw new IllegalStateException("Addon already registered: " + addon.getClass().getName());
}
addons.add(addon);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ export class XTermElement extends LitElement implements TerminalMixin {
bellStyle: 'none' | 'sound'

customKeyEventHandlers: CustomKeyEventHandlerRegistry;

addons : Object = {};

render(): TemplateResult {
return html`
Expand Down