diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 6834bf608..883b62d66 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -132,5 +132,6 @@
ui/status/bluetooth.js
ui/status/screencast.js
ui/status/system.js
+ ui/status/thunderbolt.js
diff --git a/js/ui/panel.js b/js/ui/panel.js
index d9b1a32ef..3eeb12622 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -720,7 +720,9 @@ var AggregateMenu = new Lang.Class({
this._screencast = new imports.ui.status.screencast.Indicator();
this._location = new imports.ui.status.location.Indicator();
this._nightLight = new imports.ui.status.nightLight.Indicator();
+ this._thunderbolt = new imports.ui.status.thunderbolt.Indicator();
+ this._indicators.add_child(this._thunderbolt.indicators);
this._indicators.add_child(this._screencast.indicators);
this._indicators.add_child(this._location.indicators);
this._indicators.add_child(this._nightLight.indicators);
diff --git a/js/ui/status/thunderbolt.js b/js/ui/status/thunderbolt.js
new file mode 100644
index 000000000..5f148dac1
--- /dev/null
+++ b/js/ui/status/thunderbolt.js
@@ -0,0 +1,341 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+// the following is a modified version of bolt/contrib/js/client.js
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Lang = imports.lang;
+const Shell = imports.gi.Shell;
+const Signals = imports.signals;
+
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+const PanelMenu = imports.ui.panelMenu;
+
+/* Keep in sync with data/org.freedesktop.bolt.xml */
+
+const BoltClientInterface = ' \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+';
+
+const BoltDeviceInterface = ' \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+ \
+';
+
+const BoltClientProxy = Gio.DBusProxy.makeProxyWrapper(BoltClientInterface);
+const BoltDeviceProxy = Gio.DBusProxy.makeProxyWrapper(BoltDeviceInterface);
+
+/* */
+
+var Status = {
+ DISCONNECTED: 0,
+ CONNECTED: 1,
+ AUTHORIZING: 2,
+ AUTH_ERROR: 3,
+ AUTHORIZED: 4,
+ AUTHORIZED_SECURE: 5,
+ AUTHORIZED_NEWKY: 6
+};
+
+var Policy = {
+ DEFAULT: 0,
+ MANUAL: 1,
+ AUTO:2
+};
+
+var AuthFlags = {
+ NONE: 0,
+};
+
+const BOLT_DBUS_NAME = 'org.freedesktop.bolt';
+const BOLT_DBUS_PATH = '/org/freedesktop/bolt';
+
+var Client = new Lang.Class({
+ Name: 'BoltClient',
+
+ _init: function() {
+
+ this._proxy = null;
+ new BoltClientProxy(
+ Gio.DBus.system,
+ BOLT_DBUS_NAME,
+ BOLT_DBUS_PATH,
+ Lang.bind(this, this._onProxyReady)
+ );
+
+ this.probing = false;
+ },
+
+ _onProxyReady: function(proxy, error) {
+ if (error !== null) {
+ log('error creating bolt proxy: %s'.format(error.message));
+ return;
+ }
+ this._proxy = proxy;
+ this._propsChangedId = this._proxy.connect('g-properties-changed', Lang.bind(this, this._onPropertiesChanged));
+ this._deviceAddedId = this._proxy.connectSignal('DeviceAdded', Lang.bind(this, this._onDeviceAdded), true);
+
+ this.probing = this._proxy.Probing;
+ if (this.probing)
+ this.emit('probing-changed', this.probing);
+
+ },
+
+ _onPropertiesChanged: function(proxy, properties) {
+ let unpacked = properties.deep_unpack();
+ if (!('Probing' in unpacked))
+ return;
+
+ this.probing = this._proxy.Probing;
+ this.emit('probing-changed', this.probing);
+ },
+
+ _onDeviceAdded: function(proxy, emitter, params) {
+ let [path] = params;
+ let device = new BoltDeviceProxy(Gio.DBus.system,
+ BOLT_DBUS_NAME,
+ path);
+ this.emit('device-added', device);
+ },
+
+ /* public methods */
+ close: function() {
+ if (!this._proxy)
+ return;
+
+ this._proxy.disconnectSignal(this._deviceAddedId);
+ this._proxy.disconnect(this._propsChangedId);
+ this._proxy = null;
+ },
+
+ enrollDevice: function(id, policy, callback) {
+ this._proxy.EnrollDeviceRemote(id, policy, AuthFlags.NONE,
+ Lang.bind(this, function (res, error) {
+ if (error) {
+ callback(null, error);
+ return;
+ }
+
+ let [path] = res;
+ let device = new BoltDeviceProxy(Gio.DBus.system,
+ BOLT_DBUS_NAME,
+ path);
+ callback(device, null);
+ }));
+ }
+
+});
+
+Signals.addSignalMethods(Client.prototype);
+
+/* helper class to automatically authorize new devices */
+var AuthRobot = new Lang.Class({
+ Name: 'BoltAuthRobot',
+
+ _init: function(client) {
+
+ this._client = client;
+
+ this._devicesToEnroll = [];
+ this._enrolling = false;
+
+ this._client.connect('device-added', Lang.bind(this, this._onDeviceAdded));
+ },
+
+ close: function() {
+ this.disconnectAll();
+ this._client = null;
+ },
+
+ /* the "device-added" signal will be emitted by boltd for every
+ * device that is not currently stored in the database. We are
+ * only interested in those devices, because all known devices
+ * will be handled by the user himself */
+ _onDeviceAdded: function(cli, dev) {
+ if (dev.Status !== Status.CONNECTED)
+ return;
+
+ /* check if we should enroll the device */
+ let res = [false];
+ this.emit('enroll-device', dev, res);
+ if (res[0] !== true)
+ return;
+
+ /* ok, we should authorize the device, add it to the back
+ * of the list */
+ this._devicesToEnroll.push(dev);
+ this._enrollDevices();
+ },
+
+ /* The enrollment queue:
+ * - new devices will be added to the end of the array.
+ * - an idle callback will be scheduled that will keep
+ * calling itself as long as there a devices to be
+ * enrolled.
+ */
+ _enrollDevices: function() {
+ if (this._enrolling)
+ return;
+
+ this.enrolling = true;
+ GLib.idle_add(GLib.PRIORITY_DEFAULT,
+ Lang.bind(this, this._enrollDevicesIdle));
+ },
+
+ _onEnrollDone: function(device, error) {
+ if (error)
+ this.emit('enroll-failed', error, device);
+
+ /* TODO: scan the list of devices to be authorized for children
+ * of this device and remove them (and their children and
+ * their children and ....) from the device queue
+ */
+ this._enrolling = this._devicesToEnroll.length > 0;
+
+ if (this._enrolling)
+ GLib.idle_add(GLib.PRIORITY_DEFAULT,
+ Lang.bind(this, this._enrollDevicesIdle));
+ },
+
+ _enrollDevicesIdle: function() {
+ let devices = this._devicesToEnroll;
+
+ let dev = devices.shift();
+ if (dev === undefined)
+ return GLib.SOURCE_REMOVE;
+
+ this._client.enrollDevice(dev.Uid,
+ Policy.DEFAULT,
+ Lang.bind(this, this._onEnrollDone));
+ return GLib.SOURCE_REMOVE;
+ }
+
+});
+
+Signals.addSignalMethods(AuthRobot.prototype);
+
+/* eof client.js */
+
+var Indicator = new Lang.Class({
+ Name: 'ThunderboltIndicator',
+ Extends: PanelMenu.SystemIndicator,
+
+ _init: function() {
+ this.parent();
+
+ this._indicator = this._addIndicator();
+ this._indicator.icon_name = 'thunderbolt-symbolic';
+
+ this._client = new Client();
+ this._client.connect('probing-changed', Lang.bind(this, this._onProbing));
+
+ this._robot = new AuthRobot(this._client);
+
+ this._robot.connect('enroll-device', Lang.bind(this, this._onEnrollDevice));
+ this._robot.connect('enroll-failed', Lang.bind(this, this._onEnrollFailed));
+
+ Main.sessionMode.connect('updated', Lang.bind(this, this._sync));
+ this._sync();
+
+ this._source = null;
+ },
+
+ _onDestroy: function() {
+ this._robot.close();
+ this._client.close();
+ },
+
+ _ensureSource: function() {
+ if (!this._source) {
+ this._source = new MessageTray.Source(_("Thunderbolt"),
+ 'thunderbolt-symbolic');
+ this._source.connect('destroy', Lang.bind(this, function() {
+ this._source = null;
+ }));
+
+ Main.messageTray.add(this._source);
+ }
+
+ return this._source;
+ },
+
+ _notify: function(title, body) {
+ if (this._notification)
+ this._notification.destroy();
+
+ let source = this._ensureSource();
+
+ this._notification = new MessageTray.Notification(source, title, body);
+ this._notification.setUrgency(MessageTray.Urgency.HIGH);
+ this._notification.connect('destroy', function() {
+ this._notification = null;
+ });
+ this._notification.connect('activated', () => {
+ let app = Shell.AppSystem.get_default().lookup_app('gnome-thunderbolt-panel.desktop');
+ if (app)
+ app.activate();
+ });
+ this._source.notify(this._notification);
+ },
+
+ /* Session callbacks */
+ _sync: function() {
+ let active = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+ this._indicator.visible = active && this._client.probing;
+ },
+
+
+ /* Bolt.Client callbacks */
+ _onProbing: function(cli, probing) {
+ if (probing)
+ this._indicator.icon_name = 'thunderbolt-acquiring-symbolic';
+ else
+ this._indicator.icon_name = 'thunderbolt-symbolic';
+
+ this._sync();
+ },
+
+
+ /* AuthRobot callbacks */
+ _onEnrollDevice: function(obj, device, policy) {
+ let auth = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+ policy[0] = auth;
+
+ log("thunderbolt: [%s] auto enrollment: %s".format(device.Name, auth ? 'yes' : 'no'));
+ if (auth)
+ return; /* we are done */
+
+ const title = _('Unknown Thunderbolt device');
+ const body = _('New device has been detected while you were away. Please disconnect and reconnect the device to start using it.');
+ this._notify(title, body);
+ },
+
+ _onEnrollFailed: function (obj, device, error) {
+ const title = _('Thunderbolt authorization error');
+ const body = _('Could not authorize the thunderbolt device: %s'.format(error.message));
+ this._notify(title, body);
+ }
+
+});