Skip to content

Commit a525fbe

Browse files
committed
feat: add support for named terminal add-ons
1 parent 4a19917 commit a525fbe

File tree

5 files changed

+170
-3
lines changed

5 files changed

+170
-3
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.flowingcode.vaadin.addons.xterm;
2+
3+
import com.vaadin.flow.dom.Element;
4+
import com.vaadin.flow.internal.JsonCodec;
5+
import elemental.json.Json;
6+
import elemental.json.JsonArray;
7+
import java.io.Serializable;
8+
9+
/**
10+
* Represents an abstract base class for server-side terminal add-ons that have a corresponding
11+
* client-side (JavaScript) component or require interaction with the client-side terminal
12+
* environment. It extends {@link TerminalAddon} and specializes its use for client-aware
13+
* operations.
14+
*
15+
* @author Javier Godoy / Flowing Code S.A.
16+
*/
17+
public abstract class ClientTerminalAddon extends TerminalAddon {
18+
19+
private final XTermBase xterm;
20+
21+
/**
22+
* Constructs a new {@code ClientTerminalAddon} and associates it with the specified
23+
* {@link XTermBase} instance.
24+
* <p>
25+
* This constructor ensures the add-on is registered with the terminal and verifies that the
26+
* add-on's name, as returned by {@link #getName()}, is not {@code null}. A non-null name is
27+
* required for client-side add-ons to be uniquely identified and targeted for JavaScript
28+
* execution.
29+
* </p>
30+
*
31+
* @param xterm The {@link XTermBase} instance this add-on will be attached to. Must not be
32+
* {@code null}.
33+
* @throws NullPointerException if {@code xterm} is {@code null}.
34+
* @throws IllegalStateException if {@link #getName()} returns {@code null} immediately after
35+
* superclass construction. This check relies on {@code getName()} being a static value.
36+
*/
37+
public ClientTerminalAddon(XTermBase xterm) {
38+
super(xterm);
39+
this.xterm = xterm;
40+
if (getName() == null) {
41+
throw new IllegalStateException();
42+
}
43+
}
44+
45+
/**
46+
* The xterm instance that this add-on is associated with.
47+
*/
48+
protected XTermBase getXterm() {
49+
return xterm;
50+
}
51+
52+
/**
53+
* Retrieves the unique name of this client-side add-on.
54+
* <p>
55+
* This name is used by {@link #executeJs(String, Serializable...)} to target the corresponding
56+
* JavaScript object on the client (i.e., {@code this.addons[name]} within the client-side
57+
* terminal's scope). The name effectively acts as a key in a client-side add-ons collection
58+
* managed by the terminal.
59+
* </p>
60+
*
61+
* @return the unique, non-null string identifier for the client-side counterpart of this add-on.
62+
* Subclasses must implement this to provide a name for add-on-specific JavaScript
63+
* execution.
64+
*/
65+
protected abstract String getName();
66+
67+
/**
68+
* Executes a JavaScript {@code expression} in the context of this add-on, with the specified
69+
* {@code parameters}.
70+
*
71+
* @see #getName()
72+
* @see Element#executeJs(String, Serializable...)
73+
*/
74+
protected final void executeJs(String expression, Serializable... parameters) {
75+
String name = getName();
76+
77+
JsonArray args = Json.createArray();
78+
for (int i = 0; i < parameters.length; i++) {
79+
args.set(i, JsonCodec.encodeWithTypeInfo(parameters[i]));
80+
}
81+
82+
expression = expression.replaceAll("\\$(\\d+)", "\\$1[$1]");
83+
xterm.executeJs("(function(){" + expression + "}).apply(this.addons[$0],$1);", name, args);
84+
}
85+
86+
}

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: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.flowingcode.vaadin.addons.xterm;
2+
3+
import java.util.Objects;
4+
5+
/**
6+
* Represents an abstract base class for server-side add-ons designed to extend or modify the
7+
* functionality of an {@link XTermBase} terminal instance.
8+
* <p>
9+
* Concrete add-on implementations should subclass this class to provide specific features. Each
10+
* add-on is tightly coupled with a specific {@code XTermBase} instance, allowing it to interact
11+
* with and enhance that terminal.
12+
* </p>
13+
*
14+
* @author Javier Godoy / Flowing Code S.A.
15+
*/
16+
public abstract class TerminalAddon {
17+
18+
/**
19+
* Constructs a new {@code TerminalAddon} and associates it with the provided {@link XTermBase}
20+
* instance.
21+
*
22+
* @param xterm the {@code XTermBase} instance to which this add-on will be attached.
23+
* @throws NullPointerException if the provided {@code xterm} is {@code null}.
24+
*/
25+
public TerminalAddon(XTermBase xterm) {
26+
Objects.requireNonNull(xterm);
27+
xterm.registerServerSideAddon(this);
28+
}
29+
30+
}

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

Lines changed: 47 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,47 @@ 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
315+
* the add-on 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
320+
* {@code addon} is already registered with this terminal instance.
321+
*/
322+
*/
323+
final <T extends TerminalAddon> void registerServerSideAddon(T addon) {
324+
if (getAddon(addon.getClass()) != null) {
325+
throw new IllegalStateException("Addon already registered: " + addon.getClass().getName());
326+
}
327+
addons.add(addon);
328+
}
329+
284330
}

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)