Getting side-by-side preview in a terminal app Hyper

Takuya Matsuyama - Oct 4 '21 - - Dev Community

Hi, it's Takuya.
I'm publishing YouTube content, sharing my dev workflows.

Want a side-by-side preview in a terminal app

For web coding tutorials, it'd be great to show your coding and output side-by-side.
Typically, you would do that by simply arranging two windows: An editor and a browser.
But that's annoying because every time you make a tutorial, you have to align the windows side-by-side.

Hyper, a terminal app built with web standards, once had a built-in webview feature:

Hyper's built-in webview feature

As you can see above, it displays a preview on the right side of the terminal window.
It looks pretty neat.
So, you can make tutorials without having a separate window for preview.

But it looks like the feature has been removed for security reasons:

It was 3 years ago, a quite old issue.
I understand the risk of loading web pages in Electron apps.
However, webview of the recent Electron prohibits NodeJS integration by default. So, I think it's safe to embed in Hyper, especially when you understand what you do.

Hacking Hyper to get back the built-in webview feature

So, I decided to get that feature back to Hyper, and successfully did it.
Here is how it looks like:

Demo

Demo video:

How to build

I've made built-in-webview branch for the hack:

https://github.com/craftzdog/hyper/tree/built-in-webview

Clone this repo and built it yourself as following.

npm i
npm run dev
# On another terminal session
npm run app
Enter fullscreen mode Exit fullscreen mode

How to use

Split the window right by pressing Cmd-D.
Then, type echo <URL-to-open> and hit Return.
Click the URL in the terminal.
Then, the pane becomes a webview that loads the URL.

How I hacked Hyper

Check out the diffs

First, you have to allow webview tags in app/ui/window.ts:

   const winOpts: BrowserWindowConstructorOptions = {
     minWidth: 370,
     minHeight: 190,
     backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'),
     titleBarStyle: 'hiddenInset',
     title: 'Hyper.app',
     // we want to go frameless on Windows and Linux
     frame: process.platform === 'darwin',
     transparent: process.platform === 'darwin',
     icon,
     show: Boolean(process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev),
     acceptFirstMouse: true,
     webPreferences: {
       nodeIntegration: true,
       navigateOnDragDrop: true,
       enableRemoteModule: true,
-      contextIsolation: false
+      contextIsolation: false,
+      webviewTag: true
     },
     ...options_
   };
Enter fullscreen mode Exit fullscreen mode

Hyper partially remains the old implementations. You can reuse them.
The terminal component already has url prop.
You can display webview when the component has url prop.

In lib/components/term.tsx, change the terminal component class like so:

@@ -430,18 +436,35 @@ export default class Term extends React.PureComponent<TermProps> {
         style={{padding: this.props.padding}}
         onMouseUp={this.onMouseUp}
       >
-        {this.props.customChildrenBefore}
-        <div ref={this.onTermWrapperRef} className="term_fit term_wrapper" />
-        {this.props.customChildren}
-        {this.props.search ? (
-          <SearchBox
-            search={this.search}
-            next={this.searchNext}
-            prev={this.searchPrevious}
-            close={this.closeSearchBox}
+        {this.props.url ? (
+          <webview
+            src={this.props.url}
+            style={{
+              background: '#fff',
+              position: 'absolute',
+              top: 0,
+              left: 0,
+              display: 'inline-flex',
+              width: '100%',
+              height: '100%'
+            }}
           />
         ) : (
-          ''
+          <>
+            {this.props.customChildrenBefore}
+            <div ref={this.onTermWrapperRef} className="term_fit term_wrapper" />
+            {this.props.customChildren}
+            {this.props.search ? (
+              <SearchBox
+                search={this.search}
+                next={this.searchNext}
+                prev={this.searchPrevious}
+                close={this.closeSearchBox}
+              />
+            ) : (
+              ''
+            )}
+          </>
         )}

         <style jsx global>{`
Enter fullscreen mode Exit fullscreen mode

And, change the URL click handler to dispatch an action instead of opening up the page in an external browser:

@@ -160,7 +160,13 @@ export default class Term extends React.PureComponent<TermProps> {
       this.term.loadAddon(
         new WebLinksAddon(
           (event: MouseEvent | undefined, uri: string) => {
-            if (shallActivateWebLink(event)) void shell.openExternal(uri);
+            // if (shallActivateWebLink(event)) void shell.openExternal(uri);
+            // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+            store.dispatch({
+              type: 'SESSION_URL_SET',
+              uid: props.uid,
+              url: uri
+            });
           },
           {
             // prevent default electron link handling to allow selection, e.g. via double-click
Enter fullscreen mode Exit fullscreen mode

In lib/reducers/sessions.ts, add the reducer for SESSION_URL_SET like so:

@@ -11,7 +11,8 @@ import {
   SESSION_SET_XTERM_TITLE,
   SESSION_SET_CWD,
   SESSION_SEARCH,
-  SESSION_SEARCH_CLOSE
+  SESSION_SEARCH_CLOSE,
+  SESSION_URL_SET
 } from '../constants/sessions';
 import {sessionState, session, Mutable, ISessionReducer} from '../hyper';

@@ -129,6 +130,9 @@ const reducer: ISessionReducer = (state = initialState, action) => {
       }
       return state;

+    case SESSION_URL_SET:
+      return state.setIn(['sessions', action.uid, 'url'], action.url);
+
     default:
       return state;
   }
Enter fullscreen mode Exit fullscreen mode

It works like a charm!

Follow me online

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .