Skip to content

Commit 1a5d16e

Browse files
javier-godoypaodb
authored andcommitted
feat: add support for named terminal add-ons
1 parent ae7fdcd commit 1a5d16e

File tree

5 files changed

+210
-3
lines changed

5 files changed

+210
-3
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*-
2+
* #%L
3+
* XTerm Console Addon
4+
* %%
5+
* Copyright (C) 2020 - 2025 Flowing Code
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package com.flowingcode.vaadin.addons.xterm;
21+
22+
import com.vaadin.flow.dom.Element;
23+
import com.vaadin.flow.internal.JsonCodec;
24+
import elemental.json.Json;
25+
import elemental.json.JsonArray;
26+
import java.io.Serializable;
27+
28+
/**
29+
* Represents an abstract base class for server-side terminal add-ons that have a corresponding
30+
* client-side (JavaScript) component or require interaction with the client-side terminal
31+
* environment. It extends {@link TerminalAddon} and specializes its use for client-aware
32+
* operations.
33+
*
34+
* @author Javier Godoy / Flowing Code S.A.
35+
*/
36+
@SuppressWarnings("serial")
37+
public abstract class ClientTerminalAddon extends TerminalAddon {
38+
39+
private final XTermBase xterm;
40+
41+
/**
42+
* Constructs a new {@code ClientTerminalAddon} and associates it with the specified
43+
* {@link XTermBase} instance.
44+
* <p>
45+
* This constructor ensures the add-on is registered with the terminal and verifies that the
46+
* add-on's name, as returned by {@link #getName()}, is not {@code null}. A non-null name is
47+
* required for client-side add-ons to be uniquely identified and targeted for JavaScript
48+
* execution.
49+
* </p>
50+
*
51+
* @param xterm the {@link XTermBase} instance this add-on will be attached to. Must not be
52+
* {@code null}.
53+
* @throws NullPointerException if {@code xterm} is {@code null}
54+
* @throws IllegalStateException if {@link #getName()} returns {@code null} immediately after
55+
* superclass construction. This check relies on {@code getName()} being a static value.
56+
*/
57+
protected ClientTerminalAddon(XTermBase xterm) {
58+
super(xterm);
59+
this.xterm = xterm;
60+
if (getName() == null) {
61+
throw new IllegalStateException("getName() must return a non-null value");
62+
}
63+
}
64+
65+
/**
66+
* The xterm instance that this add-on is associated with.
67+
*/
68+
protected XTermBase getXterm() {
69+
return xterm;
70+
}
71+
72+
/**
73+
* Retrieves the unique name of this client-side add-on.
74+
* <p>
75+
* This name is used by {@link #executeJs(String, Serializable...)} to target the corresponding
76+
* JavaScript object on the client (i.e., {@code this.addons[name]} within the client-side
77+
* terminal's scope). The name effectively acts as a key in a client-side add-ons collection
78+
* managed by the terminal.
79+
* </p>
80+
*
81+
* @return the unique, non-null string identifier for the client-side counterpart of this add-on.
82+
* Subclasses must implement this to provide a name for add-on-specific JavaScript
83+
* execution.
84+
*/
85+
protected abstract String getName();
86+
87+
/**
88+
* Executes a JavaScript {@code expression} in the context of this add-on, with the specified
89+
* {@code parameters}.
90+
*
91+
* @see #getName()
92+
* @see Element#executeJs(String, Serializable...)
93+
*/
94+
protected final void executeJs(String expression, Serializable... parameters) {
95+
String name = getName();
96+
97+
JsonArray args = Json.createArray();
98+
for (int i = 0; i < parameters.length; i++) {
99+
args.set(i, JsonCodec.encodeWithTypeInfo(parameters[i]));
100+
}
101+
102+
expression = expression.replaceAll("\\$(\\d+)", "\\$1[$1]");
103+
xterm.executeJs("(function(){" + expression + "}).apply(this.addons[$0],$1);", name, args);
104+
}
105+
106+
}

src/main/java/com/flowingcode/vaadin/addons/xterm/PreserveStateAddon.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* #%L
33
* XTerm Console Addon
44
* %%
5-
* Copyright (C) 2020 - 2023 Flowing Code
5+
* Copyright (C) 2020 - 2025 Flowing Code
66
* %%
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
@@ -43,7 +43,9 @@
4343
* addon.writePrompt();
4444
* </pre>
4545
*/
46-
public class PreserveStateAddon implements ITerminal, ITerminalOptions {
46+
public class PreserveStateAddon extends TerminalAddon
47+
implements ITerminal, ITerminalOptions {
48+
4749
/**
4850
* The xterm to delegate all calls to.
4951
*/
@@ -80,6 +82,7 @@ public class PreserveStateAddon implements ITerminal, ITerminalOptions {
8082
private final ITerminalOptions optionsDelegate;
8183

8284
public PreserveStateAddon(XTerm xterm) {
85+
super(xterm);
8386
this.xterm = Objects.requireNonNull(xterm);
8487
optionsMemoizer = new StateMemoizer(xterm, ITerminalOptions.class);
8588
optionsDelegate = (ITerminalOptions) optionsMemoizer.getProxy();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*-
2+
* #%L
3+
* XTerm Console Addon
4+
* %%
5+
* Copyright (C) 2020 - 2025 Flowing Code
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package com.flowingcode.vaadin.addons.xterm;
21+
22+
import java.io.Serializable;
23+
import java.util.Objects;
24+
25+
/**
26+
* Represents an abstract base class for server-side add-ons designed to extend or modify the
27+
* functionality of an {@link XTermBase} terminal instance.
28+
* <p>
29+
* Concrete add-on implementations should subclass this class to provide specific features. Each
30+
* add-on is tightly coupled with a specific {@code XTermBase} instance, allowing it to interact
31+
* with and enhance that terminal.
32+
* </p>
33+
*
34+
* @author Javier Godoy / Flowing Code S.A.
35+
*/
36+
@SuppressWarnings("serial")
37+
public abstract class TerminalAddon implements Serializable {
38+
39+
/**
40+
* Constructs a new {@code TerminalAddon} and associates it with the provided {@link XTermBase}
41+
* instance.
42+
*
43+
* @param xterm the {@code XTermBase} instance to which this add-on will be attached
44+
* @throws NullPointerException if the provided {@code xterm} is {@code null}
45+
*/
46+
protected TerminalAddon(XTermBase xterm) {
47+
Objects.requireNonNull(xterm);
48+
xterm.registerServerSideAddon(this);
49+
}
50+
51+
}

src/main/java/com/flowingcode/vaadin/addons/xterm/XTermBase.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* #%L
33
* XTerm Console Addon
44
* %%
5-
* Copyright (C) 2020 - 2023 Flowing Code
5+
* Copyright (C) 2020 - 2025 Flowing Code
66
* %%
77
* Licensed under the Apache License, Version 2.0 (the "License");
88
* you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@
4343
import java.lang.reflect.Method;
4444
import java.lang.reflect.ParameterizedType;
4545
import java.lang.reflect.Proxy;
46+
import java.util.ArrayList;
4647
import java.util.Arrays;
4748
import java.util.HashSet;
4849
import java.util.LinkedList;
@@ -69,6 +70,8 @@ public abstract class XTermBase extends Component
6970

7071
private List<Command> deferredCommands;
7172

73+
private final List<TerminalAddon> addons = new ArrayList<>();
74+
7275
private class ProxyInvocationHandler implements InvocationHandler, Serializable {
7376

7477
@Override
@@ -281,4 +284,46 @@ private Registration addCustomKeyListener(
281284
public void setEnabled(boolean enabled) {
282285
HasEnabled.super.setEnabled(enabled);
283286
}
287+
288+
/**
289+
* Retrieves a registered server-side add-on instance of a specific type.
290+
* <p>
291+
* Example usage:
292+
* </p>
293+
*
294+
* <pre>{@code
295+
* MySpecificAddon addon = terminal.getAddon(MySpecificAddon.class);
296+
* if (addon != null) {
297+
* addon.doSomethingSpecific();
298+
* }
299+
* }</pre>
300+
*
301+
* @param <T> the type of the add-on to retrieve. This is inferred from the {@code clazz}
302+
* parameter.
303+
* @param clazz the {@code Class} object representing the type of the add-on to retrieve. Must not
304+
* be {@code null}.
305+
* @return the registered add-on instance that is of the specified {@code Class<T>}, or
306+
* {@code null} if no such add-on is found
307+
* @throws NullPointerException if {@code clazz} is {@code null}
308+
*/
309+
public <T extends TerminalAddon> T getAddon(Class<? extends T> clazz) {
310+
return addons.stream().filter(clazz::isInstance).map(clazz::cast).findFirst().orElse(null);
311+
}
312+
313+
/**
314+
* Registers a server-side add-on with this terminal instance. This method is called by the add-on
315+
* itself during its construction.
316+
*
317+
* @param addon the add-on to register. Must not be {@code null}.
318+
* @throws NullPointerException if {@code addon} is {@code null}
319+
* @throws IllegalStateException if an add-on of the same class as the provided {@code addon} is
320+
* already registered with this terminal instance
321+
*/
322+
final <T extends TerminalAddon> void registerServerSideAddon(T addon) {
323+
if (getAddon(addon.getClass()) != null) {
324+
throw new IllegalStateException("Addon already registered: " + addon.getClass().getName());
325+
}
326+
addons.add(addon);
327+
}
328+
284329
}

src/main/resources/META-INF/frontend/fc-xterm/xterm-element.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ export class XTermElement extends LitElement implements TerminalMixin {
161161
bellStyle: 'none' | 'sound'
162162

163163
customKeyEventHandlers: CustomKeyEventHandlerRegistry;
164+
165+
addons : Object = {};
164166

165167
render(): TemplateResult {
166168
return html`

0 commit comments

Comments
 (0)