diff --git a/PLUGINS.md b/PLUGINS.md index 5d7c6134b0c8..3aa0abd8faca 100644 --- a/PLUGINS.md +++ b/PLUGINS.md @@ -40,6 +40,23 @@ When you put a `console.log()` in your plugin code, it will be displayed in the Almost all available API methods can be found on https://hyper.is. If there's any missing, let us know or submit a PR to document it! +### Xterm Addons + +Plugins can contribute xterm.js addons by exporting a `getXtermAddons` function that returns an array of addon instances. + +Example: + +```js +// In your plugin's main file +import { xyzAddon } from 'xterm-addon-xyz'; + +exports.getXtermAddons = () => { + return [new xyzAddon()]; +}; +``` + +These addons are loaded by Hyper and made available to the terminal. + ### Components You can decorate almost all Hyper components with a Higher-Order Component (HOC). To understand their architecture, the easiest way is to use React dev-tools to dig in to their hierarchy. diff --git a/app/plugins/extensions.ts b/app/plugins/extensions.ts index 5fd3cdfe2ed7..522644f06d4d 100644 --- a/app/plugins/extensions.ts +++ b/app/plugins/extensions.ts @@ -14,10 +14,10 @@ export const availableExtensions = new Set([ 'decorateBrowserOptions', 'decorateMenu', 'decorateTerm', + 'decorateTerms', 'decorateHyper', 'decorateHyperTerm', // for backwards compatibility with hyperterm 'decorateHeader', - 'decorateTerms', 'decorateTab', 'decorateNotification', 'decorateNotifications', @@ -31,6 +31,7 @@ export const availableExtensions = new Set([ 'getTabProps', 'getTabsProps', 'getTermGroupProps', + 'getXtermAddons', 'mapHyperTermState', 'mapTermsState', 'mapHeaderState', diff --git a/lib/components/term.tsx b/lib/components/term.tsx index 45c1464c97cc..29bd6ef795d5 100644 --- a/lib/components/term.tsx +++ b/lib/components/term.tsx @@ -19,7 +19,7 @@ import {WebglAddon} from 'xterm-addon-webgl'; import type {TermProps} from '../../typings/hyper'; import terms from '../terms'; import processClipboard from '../utils/paste'; -import {decorate} from '../utils/plugins'; +import {decorate, getXtermAddons} from '../utils/plugins'; import _SearchBox from './searchBox'; @@ -243,6 +243,16 @@ export default class Term extends React.PureComponent< if (props.imageSupport) { this.term.loadAddon(new ImageAddon()); } + + // Load xterm addons from plugins + const pluginAddons = getXtermAddons(); + for (const addon of pluginAddons) { + try { + this.term.loadAddon(addon); + } catch (err) { + console.warn(`Failed to load xterm addon from plugin:`, err); + } + } } else { // get the cached plugins this.fitAddon = props.fitAddon!; diff --git a/lib/utils/plugins.ts b/lib/utils/plugins.ts index dea23f915fdc..5997b4364ff4 100644 --- a/lib/utils/plugins.ts +++ b/lib/utils/plugins.ts @@ -69,6 +69,9 @@ let reducersDecorators: { reduceTermGroups: ITermGroupReducer[]; }; +// xterm addons from plugins +let xtermAddons: any[] = []; + // expose decorated component instance to the higher-order components function exposeDecorated

>( Component_: React.ComponentType

@@ -252,6 +255,7 @@ const loadModules = () => { tabsPropsDecorators = []; termPropsDecorators = []; termGroupPropsDecorators = []; + xtermAddons = []; propsDecorators = { getTermProps: termPropsDecorators, @@ -355,6 +359,15 @@ const loadModules = () => { connectors.Notifications.dispatch.push(mod.mapNotificationsDispatch); } + if (mod.getXtermAddons) { + const addons = mod.getXtermAddons(); + if (Array.isArray(addons)) { + xtermAddons.push(...addons); + } else if (addons) { + xtermAddons.push(addons); + } + } + if (mod.getTermGroupProps) { termGroupPropsDecorators.push(mod.getTermGroupProps); } @@ -581,3 +594,7 @@ export const middleware: Middleware<{}, HyperState, Dispatch> = (s remaining.length ? remaining[0](store)(nextMiddleware(remaining.slice(1)))(action_) : next(action_); nextMiddleware(middlewares)(action); }; + +export function getXtermAddons(): any[] { + return xtermAddons; +} diff --git a/package.json b/package.json index f0d433ff23ba..658553c7cfde 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "@types/lodash": "^4.17.0", "@types/mousetrap": "1.6.15", "@types/ms": "0.7.34", - "@types/node": "18.19.31", + "@types/node": "^18.19.31", "@types/plist": "3.0.5", "@types/react": "18.2.79", "@types/react-dom": "18.2.25", @@ -134,7 +134,7 @@ "style-loader": "4.0.0", "terser": "5.30.3", "terser-webpack-plugin": "^5.3.10", - "ts-node": "10.9.2", + "ts-node": "^10.9.2", "typescript": "5.4.5", "webpack": "5.91.0" }, diff --git a/test/unit/xterm-addons.test.ts b/test/unit/xterm-addons.test.ts new file mode 100644 index 000000000000..e69a6faa9ca9 --- /dev/null +++ b/test/unit/xterm-addons.test.ts @@ -0,0 +1,8 @@ +// Test for xterm addons support from plugins +import test from 'ava'; +import {getXtermAddons} from '../../lib/utils/plugins'; + +test('getXtermAddons returns empty array initially', (t) => { + const addons = getXtermAddons(); + t.deepEqual(addons, []); +}); \ No newline at end of file diff --git a/typings/hyper.d.ts b/typings/hyper.d.ts index dcc48f54a802..b937107116ff 100644 --- a/typings/hyper.d.ts +++ b/typings/hyper.d.ts @@ -145,6 +145,7 @@ export type hyperPlugin = { getTabsProps: any; getTermGroupProps: any; getTermProps: any; + getXtermAddons: any; mapHeaderDispatch: any; mapHyperDispatch: any; mapHyperTermDispatch: any;