Skip to content

Instantly share code, notes, and snippets.

@WebReflection
Last active July 29, 2022 12:01
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save WebReflection/7ab0addec037508cc8380a9c37d285f2 to your computer and use it in GitHub Desktop.
Save WebReflection/7ab0addec037508cc8380a9c37d285f2 to your computer and use it in GitHub Desktop.
A GJS WebKit JSON Communication Channel Example
const JSONChannel = (Private => class JSONChannel {
// (c) Andrea Giammarchi - @WebReflection (ISC)
constructor(secret=Array.from(
crypto.getRandomValues(new Uint8Array(8))
).map(i => i.toString(36)).join('')) {
const listener = e => this.emit('message', null, e.detail);
document.addEventListener(`${secret}:gjs`, listener);
Private.set(this, {
secret, listener,
fn: Object.create(null)
});
}
close() {
const ref = Private.get(this);
Private.delete(this);
document.removeEventListener(`${ref.secret}:gjs`, ref.listener);
}
send(data) {
document.title = `${Private.get(this).secret}:js=${JSON.stringify(data)}`;
// one day the following should work instead
// webkit.messageHandlers[Private.get(this).secret].postMessage(data);
}
emit(type, err, data) {
const listeners = Private.get(this);
if (type in listeners) listeners[type].forEach(
fn => fn.call(this, err, data)
);
}
on(type, fn) {
const listeners = Private.get(this);
if (!(type in listeners)) listeners[type] = new Set;
listeners[type].add(fn);
return this;
}
removeListener(type, fn) {
const listeners = Private.get(this);
if (type in listeners) listeners[type].delete(fn);
return this;
}
})(new WeakMap);
@WebReflection
Copy link
Author

WebReflection commented Nov 1, 2017

Example

const PageChannel = new JSONChannel();
PageChannel
  .on('message', (err, data) => {
    if (err) console.error(err);
    else console.log(data);
  })
  .send('ping');

GJS side ...

webView.connect('notify::title', (self, params) => {
  if (/^([a-z0-9]+):js=/.test(self.title)) {
    const secret = RegExp.$1;
    const data = JSON.parse(self.title.slice(secret.length + 4));
    print(data);
    self.run_javascript(
      `document.dispatchEvent(
        new CustomEvent(
          '${secret}:gjs',
          {detail: ${JSON.stringify('pong')}}
        )
      );`,
      null,
      (self, result, error) => {
        self.run_javascript_finish(result);
      }
    );
  }
});

basic html example

<!doctype html>
<script src="browser.js"></script>

@WebReflection
Copy link
Author

Alternative full GJS program

#!/usr/bin/env gjs
imports.gi.versions.Gtk = '3.0';

((GLib, Gtk, Gdk, WebKit2) => {'use strict';

  Gtk.init(null);

  const
    param = (name, value) => {
      const re = new RegExp('^--' + name + '(=.*)?$', 'i');
      return ARGV.some(p => re.test(p)) ? RegExp.$1.slice(1) : value;
    },
    Screen = Gdk.Screen.get_default(),
    FULLSCREEN = param('fullscreen', false) !== false,
    WIDTH = +param('width', FULLSCREEN ? Screen.get_width() : 480),
    HEIGHT = +param('height', FULLSCREEN ? Screen.get_height() : 320),
    CURRENT_DIR = GLib.get_current_dir(),
    PATH_SEPARATOR = /^\//.test(CURRENT_DIR) ? '/' : '\\',
    window = new Gtk.Window({
      title: 'GJS Unframed Browser',
      type : Gtk.WindowType.TOPLEVEL,
      decorated: false,
      window_position: Gtk.WindowPosition.CENTER
    }),
    webView = new WebKit2.WebView(),
    wvSettings = webView.get_settings(),
    wvUCM = webView.get_user_content_manager(),
    gtkSettings = Gtk.Settings.get_default()
  ;

  const black = new Gdk.RGBA();
  black.parse('rgb(0,0,0)');
  webView.set_background_color(black);

  const secret = randomString(8);
  const validSecret = new RegExp(`^(${secret}):js=`);
  webView.connect('load-changed', (self, loadEvent, data) => {
    switch (loadEvent) {
      case WebKit2.LoadEvent.COMMITTED:
      case WebKit2.LoadEvent.FINISHED:
        self.run_javascript(
          `dispatchEvent(new CustomEvent('gjs:ready', {detail:new JSONChannel('${secret}')}));`,
          null,
          (self, result, error) => {
            self.run_javascript_finish(result);
          }
        );
        break;
    }
  });

  webView.connect('notify::title', (self, params) => {
    if (validSecret.test(self.title)) {
      const secret = RegExp.$1;
      const data = JSON.parse(self.title.slice(secret.length + 4));
      print(data);
      self.run_javascript(
        `document.dispatchEvent(
          new CustomEvent(
            '${secret}:gjs',
            {detail: ${JSON.stringify('pong')}}
          )
        );`,
        null,
        (self, result, error) => {
          self.run_javascript_finish(result);
        }
      );
    }
  });
  // one day the following should work instead
  if (wvUCM.register_script_message_handler(secret)) {
    wvUCM.connect('script-message-received', (self, js) => {
      print(js);
    });
  }

  window.set_default_size(WIDTH, HEIGHT);
  gtkSettings.gtk_application_prefer_dark_theme = true;
  gtkSettings.gtk_theme_name = 'Adwaita';
  [
    'allow-file-access-from-file-urls',
    'allow-universal-access-from-file-urls',
    'enable_webgl',
    'enable_webaudio',
    'enable_accelerated_compositing',
    'javascript-can-access-clipboard',
    'javascript-can-open-windows-automatically'
  ].forEach(function (key) {
    wvSettings[key] = true;
  });
  // longest method name in history?
  wvSettings.set_enable_write_console_messages_to_stdout(true);
  webView.load_uri(param('uri', ['file:', '', '', GLib.get_current_dir(), 'index.html'].join(PATH_SEPARATOR)));
  window.connect('show', () => {
    if (FULLSCREEN) window.fullscreen();
    Gtk.main();
  });
  window.connect('destroy', () => Gtk.main_quit());
  window.connect('delete_event', () => false);
  window.add(webView);
  window.show_all();

  function randomString(size) {
    let chars = [];
    while (size--) chars[size] = GLib.random_int_range(0, 0xFF).toString(36);
    return chars.join('');
  }

})(
  imports.gi.GLib,
  imports.gi.Gtk,
  imports.gi.Gdk,
  imports.gi.WebKit2
);

Alternative page code

addEventListener(
  'gjs:ready',
  e => {
    const GJSChannel = e.detail;
    GJSChannel
      .on('message', (err, data) => {
        if (err) console.error(err);
        else console.log(data);
      })
      .send('ping');
  },
  {once: true}
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment