diff --git a/.gitignore b/.gitignore
index 1d21ef08785940014aa818162bc68d5d07c11324..95cedbc9140774598a570d6f0ca36692f19380df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,16 @@
-# Idea
+# IDE
/build/
/nbproject/
/.idea/
*.iml
-# Eclipse
.project
.pydevproject
.settings/
+
+.vscode
+
+# Our stuff
+po/*.po~
+*.pyc
+/builddir
diff --git a/CMakeLists.txt b/CMakeLists.txt
deleted file mode 100644
index 170c9f755802737674f1325eea22ce4d5e4cf6d3..0000000000000000000000000000000000000000
--- a/CMakeLists.txt
+++ /dev/null
@@ -1,305 +0,0 @@
-# GNOME Shell integration for Chrome build script
-
-cmake_minimum_required (VERSION 2.8)
-project (chrome-gnome-shell NONE)
-
-# Suppress warning
-if(NOT DEFINED CMAKE_SIZEOF_VOID_P)
- set(CMAKE_SIZEOF_VOID_P 8)
-endif(NOT DEFINED CMAKE_SIZEOF_VOID_P)
-
-# Variables
-set(PROJECT_VERSION "10.1")
-
-set(ARCHIVE_NAME ${CMAKE_PROJECT_NAME}-${PROJECT_VERSION})
-set(ARCHIVE_FULL_NAME ${ARCHIVE_NAME}.tar.xz)
-set(ARCHIVE_DEB_NAME ${CMAKE_PROJECT_NAME}_${PROJECT_VERSION}.orig.tar.xz)
-set(DEB_DIR ${CMAKE_CURRENT_BINARY_DIR}/deb)
-
-set(EXTENSION_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/extension)
-set(EXTENSION_BUILD_DIR ${CMAKE_BINARY_DIR}/extension)
-set(CHROME_BUILD_DIR ${EXTENSION_BUILD_DIR}/chrome)
-set(OPERA_BUILD_DIR ${EXTENSION_BUILD_DIR}/opera)
-set(FIREFOX_BUILD_DIR ${EXTENSION_BUILD_DIR}/firefox)
-
-# Options
-option(BUILD_EXTENSION "Build extension zip package" TRUE)
-option(BUILD_CONNECTOR "Build native messaging host" TRUE)
-option(BUILD_SOURCE_PACKAGE "Build source package" FALSE)
-option(BUILD_DEB "Build debian package" FALSE)
-option(BUILD_MESSAGES "Update translation strings" FALSE)
-option(USE_DEBIAN_LAYOUT "Use --install-layout=deb distutils parameter" FALSE)
-
-# Default extension key for Chrome web store
-if(NOT DEFINED CHROME_EXTENSION_KEY)
- set(CHROME_EXTENSION_KEY "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlig8TAPPQZinMkJnptC0ldizx6fG9jSjZDJ9c8GuLcXeGRH+NMlQuPC9bR5IQlT7+4VY/1tm1+IZ4xvITx1wXCNTR+KXzZv3VNc2D+logehK7oIRTRj0fLhixrx4NLSNK7L7HgV2xcIoW6QV0jOdFcTPL0mWXodXSzZePrvXuflF7qpwNxLzYVi04Vh3xu2oR2Pc9SwfZ4SNbyCaunH/p8n5AYmDuogI2Ah++RZw0ctnqn7mmHrGXteBu/vkpcHZu3B3eW9PFSrv69rRs8duybYR9C91hJm6yzRqZqIpindIU3k2HnNWeCFWkRVpZPhaNVoxcBUO7wWUUwdIflW2JwIDAQAB")
-endif(NOT DEFINED CHROME_EXTENSION_KEY)
-
-# Default extension key for Opera addons
-if(NOT DEFINED OPERA_EXTENSION_KEY)
- set(OPERA_EXTENSION_KEY "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1QBzvBFxSTBP1Z3u+B3GnxOhZiT/LLGJ9R3Mu3R5ap1YgvbHNqSj0CmrWE+MvJUYA+AxHSRP6ctKfPBZl/kdGYGLPgvagGBEEbutCerj9www7T2LAsFNc5gIgDXaU0P74Yik8MjLZIXOC3Q91Kien5Jbtit382HNVHl2nRbhgGZ9LZ+6UJnzSseW9NHw0/XRRnT0kv0Ih7lgC55xWfP7guam1uaT5DPxC7W5cy9V1z7mljBf50OxgnbEmf7Xvfz4QpQfDyQWE5hqKlBuPc8W/8bvYmWN+FDBqhls/FCml3icAElNMBg0YryFEDzu6xCTzgHqDKYu5SN49u+m6tyamQIDAQAB")
-endif(NOT DEFINED OPERA_EXTENSION_KEY)
-
-# Debian variables
-if(NOT DEFINED DEBIAN_VERSION)
- set(DEBIAN_VERSION "0ubuntu1")
-endif(NOT DEFINED DEBIAN_VERSION)
-
-if(NOT DEFINED DEBIAN_DISTRO)
- set(DEBIAN_DISTRO "trusty")
-endif(NOT DEFINED DEBIAN_DISTRO)
-
-macro(find_program_ex)
- string(TOUPPER ${ARGV0} _PROGRAM_UPPER)
-
- set(_NAMES ${ARGV})
- math(EXPR _NAMES_LAST_INDEX "${ARGC}-1")
- list(GET _NAMES ${_NAMES_LAST_INDEX} _MESSAGE_STATUS)
- list(REMOVE_AT _NAMES ${_NAMES_LAST_INDEX})
-
- find_program(${_PROGRAM_UPPER}_EXECUTABLE NAMES ${_NAMES})
-
- if(${_PROGRAM_UPPER}_EXECUTABLE)
- message(STATUS "Found ${ARGV0}: ${${_PROGRAM_UPPER}_EXECUTABLE}")
- set(${_PROGRAM_UPPER}_FOUND TRUE)
- else(${_PROGRAM_UPPER}_EXECUTABLE)
- message(${_MESSAGE_STATUS} "Could NOT find ${ARGV0}.")
- endif(${_PROGRAM_UPPER}_EXECUTABLE)
-endmacro(find_program_ex)
-
-include(GNUInstallDirs)
-
-# Options validating
-if(NOT BUILD_EXTENSION AND NOT BUILD_CONNECTOR AND NOT BUILD_MESSAGES)
- message(FATAL_ERROR "No build options selected")
-endif(NOT BUILD_EXTENSION AND NOT BUILD_CONNECTOR AND NOT BUILD_MESSAGES)
-
-if(BUILD_DEB AND NOT BUILD_SOURCE_PACKAGE)
- message(STATUS "Building of debian package enabled. Turning on building source package.")
- set(BUILD_SOURCE_PACKAGE TRUE)
-endif(BUILD_DEB AND NOT BUILD_SOURCE_PACKAGE)
-
-if(BUILD_SOURCE_PACKAGE AND NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/.git/)
- message(FATAL_ERROR "Unable to build source package outside of git repository.")
-endif(BUILD_SOURCE_PACKAGE AND NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/.git/)
-
-if(BUILD_MESSAGES AND NOT DEFINED GETTEXT_REPORT_EMAIL)
- message(FATAL_ERROR "GETTEXT_REPORT_EMAIL must be specified to generate po template.")
-endif(BUILD_MESSAGES AND NOT DEFINED GETTEXT_REPORT_EMAIL)
-
-if(BUILD_EXTENSION OR BUILD_CONNECTOR)
- find_program_ex(base64 gbase64 FATAL_ERROR)
- find_program_ex(sha256sum gsha256sum FATAL_ERROR)
- find_program_ex(ghead head FATAL_ERROR)
- find_program_ex(tr gtr FATAL_ERROR)
- find_program_ex(jq FATAL_ERROR)
-
- # https://github.com/adobe/chromium/blob/master/chrome/common/extensions/extension.cc#L696
- # http://stackoverflow.com/questions/23873623/obtaining-chrome-extension-id-for-development
- execute_process(COMMAND echo "${CHROME_EXTENSION_KEY}"
- COMMAND ${BASE64_EXECUTABLE} -d
- COMMAND ${SHA256SUM_EXECUTABLE}
- COMMAND ${GHEAD_EXECUTABLE} -c32
- COMMAND ${TR_EXECUTABLE} 0-9a-f a-p
- OUTPUT_VARIABLE CHROME_EXTENSION_ID)
- message(STATUS "Calculated Chrome extension id: ${CHROME_EXTENSION_ID}")
-
- execute_process(COMMAND echo "${OPERA_EXTENSION_KEY}"
- COMMAND ${BASE64_EXECUTABLE} -d
- COMMAND ${SHA256SUM_EXECUTABLE}
- COMMAND ${GHEAD_EXECUTABLE} -c32
- COMMAND ${TR_EXECUTABLE} 0-9a-f a-p
- OUTPUT_VARIABLE OPERA_EXTENSION_ID)
- message(STATUS "Calculated Opera extension id: ${OPERA_EXTENSION_ID}")
-endif(BUILD_EXTENSION OR BUILD_CONNECTOR)
-
-# Options handling
-if(BUILD_EXTENSION)
- find_program_ex(7z FATAL_ERROR)
-
- file(COPY "${EXTENSION_SOURCES}/" DESTINATION "${CHROME_BUILD_DIR}"
- PATTERN "manifest*.json" EXCLUDE)
-
- file(COPY "${EXTENSION_SOURCES}/" DESTINATION "${OPERA_BUILD_DIR}"
- PATTERN "manifest*.json" EXCLUDE)
-
- file(COPY "${EXTENSION_SOURCES}/" DESTINATION "${FIREFOX_BUILD_DIR}"
- PATTERN "manifest*.json" EXCLUDE)
-
- set(PUBLIC_KEY ${CHROME_EXTENSION_KEY})
- configure_file("${EXTENSION_SOURCES}/manifest.json" "${CHROME_BUILD_DIR}/")
-
- set(PUBLIC_KEY ${OPERA_EXTENSION_KEY})
- configure_file("${EXTENSION_SOURCES}/manifest.json" "${OPERA_BUILD_DIR}/")
-
- unset(PUBLIC_KEY)
-
- add_custom_target(chrome-extension ALL
- COMMAND "${7Z_EXECUTABLE}" a -tzip "${CMAKE_BINARY_DIR}/extension-chrome.zip" ./
- WORKING_DIRECTORY "${CHROME_BUILD_DIR}")
- add_custom_target(opera-extension ALL
- COMMAND "${7Z_EXECUTABLE}" a -tzip "${CMAKE_BINARY_DIR}/extension-opera.zip" ./
- WORKING_DIRECTORY "${OPERA_BUILD_DIR}")
- add_custom_target(firefox-extension ALL
- COMMAND "${JQ_EXECUTABLE}" -s "'add|del(.key)'"
- '${EXTENSION_SOURCES}/manifest.json' '${EXTENSION_SOURCES}/manifest.firefox.json'
- > "${FIREFOX_BUILD_DIR}/manifest.json"
- COMMAND "${7Z_EXECUTABLE}" a -tzip "${CMAKE_BINARY_DIR}/extension-firefox.zip" ./
- WORKING_DIRECTORY "${FIREFOX_BUILD_DIR}")
-endif(BUILD_EXTENSION)
-
-if(BUILD_CONNECTOR)
- find_package(PythonInterp REQUIRED)
-
- set(CONNECTOR_SETUP "${CMAKE_CURRENT_SOURCE_DIR}/connector/setup.py")
- add_custom_target(build-connector ALL
- COMMAND ${PYTHON_EXECUTABLE} ${CONNECTOR_SETUP} build)
-
- configure_file("${CMAKE_CURRENT_SOURCE_DIR}/connector/org.gnome.chrome_gnome_shell.json"
- "${CMAKE_BINARY_DIR}/")
- configure_file("${CMAKE_CURRENT_SOURCE_DIR}/connector/org.gnome.ChromeGnomeShell.service.in"
- "${CMAKE_BINARY_DIR}/org.gnome.ChromeGnomeShell.service")
-
- add_custom_target(firefox-native-manifest ALL
- COMMAND "${JQ_EXECUTABLE}" -s "'add|del(.allowed_origins)'"
- "${CMAKE_BINARY_DIR}/org.gnome.chrome_gnome_shell.json"
- "${CMAKE_CURRENT_SOURCE_DIR}/connector/org.gnome.chrome_gnome_shell.firefox.json"
- > "${CMAKE_BINARY_DIR}/org.gnome.chrome_gnome_shell.firefox.json")
-
- if(USE_DEBIAN_LAYOUT)
- set(DISTUTILS_EXTRA_ARGS "--install-layout=deb")
- else()
- set(DISTUTILS_EXTRA_ARGS "")
- endif(USE_DEBIAN_LAYOUT)
-
- install(CODE "
- if(DEFINED ENV{DESTDIR})
- set(DESTDIR \"\$ENV{DESTDIR}\")
- else(DEFINED ENV{DESTDIR})
- set(DESTDIR \"/\")
- endif(DEFINED ENV{DESTDIR})
-
- execute_process(COMMAND ${PYTHON_EXECUTABLE} ${CONNECTOR_SETUP}
- install ${DISTUTILS_EXTRA_ARGS}
- --root \"\${DESTDIR}\"
- --prefix \"${CMAKE_INSTALL_PREFIX}\")")
-
- if(NOT CMAKE_SYSTEM_NAME MATCHES "DragonFly.*|FreeBSD")
- install(FILES "${CMAKE_BINARY_DIR}/org.gnome.chrome_gnome_shell.json" DESTINATION "/etc/chromium/native-messaging-hosts/")
- install(FILES "${CMAKE_BINARY_DIR}/org.gnome.chrome_gnome_shell.json" DESTINATION "/etc/opt/chrome/native-messaging-hosts/")
- else(NOT CMAKE_SYSTEM_NAME MATCHES "DragonFly.*|FreeBSD")
- # FreeBSD uses patch that forces Chromium to look into undocumented
- # "/usr/local/etc/chrome/native-messaging-hosts" folder for native messaging host manifest.
- # https://github.com/freebsd/freebsd-ports/blob/master/www/chromium/files/patch-chrome_common_chrome__paths.cc
- install(FILES "${CMAKE_BINARY_DIR}/org.gnome.chrome_gnome_shell.json" DESTINATION "/usr/local/etc/chrome/native-messaging-hosts/")
- endif(NOT CMAKE_SYSTEM_NAME MATCHES "DragonFly.*|FreeBSD")
-
- install(FILES "${CMAKE_BINARY_DIR}/org.gnome.chrome_gnome_shell.firefox.json"
- DESTINATION "${CMAKE_INSTALL_LIBDIR}/mozilla/native-messaging-hosts/"
- RENAME "org.gnome.chrome_gnome_shell.json")
-
- install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/connector/org.gnome.ChromeGnomeShell.desktop" DESTINATION "${CMAKE_INSTALL_DATADIR}/applications/")
- install(FILES "${CMAKE_BINARY_DIR}/org.gnome.ChromeGnomeShell.service" DESTINATION "${CMAKE_INSTALL_DATADIR}/dbus-1/services/")
-
- install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/extension/icons/GnomeLogo-16.png"
- DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/16x16/apps/"
- RENAME org.gnome.ChromeGnomeShell.png)
- install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/extension/icons/GnomeLogo-48.png"
- DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/48x48/apps/"
- RENAME org.gnome.ChromeGnomeShell.png)
- install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/extension/icons/GnomeLogo-128.png"
- DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/128x128/apps/"
- RENAME org.gnome.ChromeGnomeShell.png)
-endif(BUILD_CONNECTOR)
-
-if(BUILD_SOURCE_PACKAGE)
- set(MESSAGE_NO_DIST "Unable to generate dist target. Dependencies missing.")
-
- find_program_ex(xz WARNING)
- find_package(Git)
-
- if(GIT_FOUND AND XZ_FOUND)
- # http://agateau.com/2009/cmake-and-make-dist-the-simple-version/
- add_custom_target(dist
- COMMAND ${GIT_EXECUTABLE} archive --prefix=${ARCHIVE_NAME}/ HEAD | ${XZ_EXECUTABLE} -z > ${CMAKE_BINARY_DIR}/${ARCHIVE_FULL_NAME}
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
- else(GIT_FOUND AND XZ_FOUND)
- if(BUILD_DEB)
- message(FATAL_ERROR ${MESSAGE_NO_DIST})
- else(BUILD_DEB)
- message(WARNING ${MESSAGE_NO_DIST})
- endif(BUILD_DEB)
- endif(GIT_FOUND AND XZ_FOUND)
-
- if(BUILD_DEB)
- find_program_ex(debuild FATAL_ERROR)
- find_program_ex(tar FATAL_ERROR)
-
- if(GPG_KEY)
- message(STATUS "Using GPG key ${GPG_KEY}")
- set(DEBUILD_KEY "-k${GPG_KEY}")
- else(GPG_KEY)
- message(STATUS "GPG_KEY not set. Using default GPG key")
- endif(GPG_KEY)
-
- configure_file("${CMAKE_CURRENT_SOURCE_DIR}/contrib/ubuntu/changelog"
- "${CMAKE_BINARY_DIR}/debian_changelog")
-
- add_custom_target(deb_prepare
- DEPENDS dist
- COMMAND ${CMAKE_COMMAND} -E make_directory "${DEB_DIR}/${ARCHIVE_NAME}"
- COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_BINARY_DIR}/${ARCHIVE_FULL_NAME}" "${DEB_DIR}/${ARCHIVE_DEB_NAME}"
- COMMAND ${TAR_EXECUTABLE} -xvf "${DEB_DIR}/${ARCHIVE_DEB_NAME}" -C "${DEB_DIR}/${ARCHIVE_NAME}" --strip-components=1)
-
- add_custom_target(deb ALL
- DEPENDS deb_prepare
- COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/contrib/ubuntu" "${DEB_DIR}/${ARCHIVE_NAME}/debian"
- COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_BINARY_DIR}/debian_changelog" "${DEB_DIR}/${ARCHIVE_NAME}/debian/changelog"
- COMMAND ${DEBUILD_EXECUTABLE} -S ${DEBUILD_KEY}
- WORKING_DIRECTORY ${DEB_DIR}/${ARCHIVE_NAME})
- endif(BUILD_DEB)
-endif(BUILD_SOURCE_PACKAGE)
-
-if(BUILD_MESSAGES)
- find_package(Gettext)
- if(NOT GETTEXT_FOUND)
- message(FATAL_ERROR "Gettext not found")
- endif(NOT GETTEXT_FOUND)
-
- add_custom_target(generate-pot
- COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/contrib/chrome-messages2po.py
- --email ${GETTEXT_REPORT_EMAIL}
- --reference-lang en
- --write-pot
- ${CMAKE_CURRENT_SOURCE_DIR}/contrib/chrome-web-store/
- ${CMAKE_CURRENT_SOURCE_DIR}/extension/_locales/
- ${CMAKE_CURRENT_SOURCE_DIR}/po/)
-
- add_custom_target(update-po
- DEPENDS generate-pot)
-
- file(GLOB PO_FILES
- LIST_DIRECTORIES false
- RELATIVE ${CMAKE_CURRENT_SOURCE_DIR}/po/
- ${CMAKE_CURRENT_SOURCE_DIR}/po/*.po)
-
- foreach(PO ${PO_FILES})
- add_custom_command(TARGET update-po
- COMMENT "Updating ${PO}"
- WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/po
- COMMAND ${GETTEXT_MSGMERGE_EXECUTABLE} -q --update
- ${PO}
- ${CMAKE_CURRENT_SOURCE_DIR}/po/chrome-gnome-shell.pot)
- endforeach()
-
- add_custom_command(TARGET update-po
- COMMAND ${CMAKE_COMMAND} -E remove ${CMAKE_CURRENT_SOURCE_DIR}/po/*.po~)
-
- add_custom_target(build-messages ALL
- DEPENDS update-po
- COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/contrib/po2chrome-messages.py
- --reference-lang en
- ${CMAKE_CURRENT_SOURCE_DIR}/contrib/chrome-web-store/
- ${CMAKE_CURRENT_SOURCE_DIR}/extension/_locales/
- ${CMAKE_CURRENT_SOURCE_DIR}/po/)
-endif(BUILD_MESSAGES)
diff --git a/README.md b/README.md
index 4fd48dd13f5a5fac47e7b3724e4cafa901b7437d..832756757cefe53b2509f8a0ddc17c55f9cd7919 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,58 @@
-GNOME Shell integration for Chrome
-============================================
-[](https://landscape.io/github/nE0sIghT/chrome-gnome-shell-mirror/master)
+# GNOME Shell browser extension
+## Introduction
-Introduction
-------------
+This repository contains browser extension that provides integration with GNOME Shell and the corresponding
+extensions repository https://extensions.gnome.org/.
-This repository contains Browser extension for Google Chrome/Chromium, Firefox, Vivaldi, Opera (and other
-Browser Extension, Chrome Extension or WebExtensions capable browsers) and native host messaging connector
-that provides integration with GNOME Shell and the corresponding extensions repository https://extensions.gnome.org/.
+This extension works in conjuction with [os-native counterpart](https://gitlab.gnome.org/nE0sIghT/gnome-browser-connector).
-Requirements
-------------
- * GNOME Shell
- * Python 2.7+ or 3.x
- * PyGObject
- * Python Requests >= 2.4.2
+## Build
-Installation
-------------
+First you need to install build requirements:
+- meson
+- python3
+- gettext
+- p7zip
+- polib python module
-Recent installation instructions are [available in GNOME wiki](https://wiki.gnome.org/Projects/GnomeShellIntegrationForChrome/Installation).
+Then invoke meson to build extension:
+```shell
+ meson builddir
+ cd builddir
+ meson compile
+```
+
+This will produce 2 zip files in `builddir` folder:
+- extension-chrome.zip
+- extension-firefox
+
+Those files can be uploaded to corresponding browser Addons website.
+There are also unpacked extensions available under `builddir/extension` folder that can be loaded in web browsers for development purposes.
+
+## Translations
+
+This project uses [GNOME Translation Project](https://wiki.gnome.org/TranslationProject). Translation statistics can be obtained on corresponding Damned Lies page.
+
+### Translation strings handling
+
+All user-visible web extension strings are maintained by developers in messages.json file at `extension/extension/_locale/en/` folder. File format is described [here](https://developer.chrome.com/extensions/i18n-messages).
+
+Gettext template located in `po/chrome-gnome-shell.pot` and auto generated using meson:
+
+```shell
+ meson -Dbuild_messages=true -Dbuild_extension=false builddir
+ cd builddir
+ meson compile
+```
+
+As result of those commands:
+1. New gettext template will be saved to `po/chrome-gnome-shell.pot` file.
+2. All `po/*.po` files will be updated by `msgmerge` using new gettext template.
+3. All extension locales (messages.json) will be generated from gettext po files.
+
+This process is fully compatible with Damned Lies.
+
+There is limited number of [supported locales](https://developer.chrome.com/webstore/i18n?csw=1#localeTable).
+If you use an unsupported locale, Google Chrome will ignore it.
+
+To create a new translation you can use msginit command (or any po editor) and gettext template. Please refer [Translation Project wiki page](https://wiki.gnome.org/TranslationProject) for further info about optimal translation workflow.
diff --git a/chrome-gnome-shell.doap b/chrome-gnome-shell.doap
index d6d2973af081289340f3145e15dca9aca2d20fd7..629d0daffb04a44fed8c28d4b44077c7b3831278 100644
--- a/chrome-gnome-shell.doap
+++ b/chrome-gnome-shell.doap
@@ -5,20 +5,15 @@
xmlns:gnome="http://api.gnome.org/doap-extensions#"
xmlns="http://usefulinc.com/ns/doap#">
- chrome-gnome-shell
- GNOME Shell integration for Chrome
+ gnome-browser-extension
+ GNOME Shell browser extension
- Browser extension for Google Chrome/Chromium, Firefox, Vivaldi, Opera (and other Browser Extension,
- Chrome Extension or WebExtensions capable browsers) and native host messaging connector that provides
- integration with GNOME Shell and the corresponding extensions repository https://extensions.gnome.org.
+ Browser extension that provides integration with GNOME Shell and the corresponding
+ extensions repository https://extensions.gnome.org/.
-
-
+
JavaScript
Python
diff --git a/connector/chrome-gnome-shell.py b/connector/chrome-gnome-shell.py
deleted file mode 100755
index 9cfc57a4085552e8cbbb78137445b9d6dc1d9eb4..0000000000000000000000000000000000000000
--- a/connector/chrome-gnome-shell.py
+++ /dev/null
@@ -1,567 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: UTF-8 -*-
-
-"""
- GNOME Shell integration for Chrome
- Copyright (C) 2016-2019 Yuri Konotopov
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-"""
-
-from __future__ import unicode_literals
-from __future__ import print_function
-from gi.repository import GLib, Gio
-import json
-import os
-import re
-import signal
-import struct
-import sys
-import traceback
-
-CONNECTOR_VERSION = 10
-DEBUG_ENABLED = False
-
-SHELL_SCHEMA = "org.gnome.shell"
-ENABLED_EXTENSIONS_KEY = "enabled-extensions"
-EXTENSION_DISABLE_VERSION_CHECK_KEY = "disable-extension-version-validation"
-DISABLE_USER_EXTENSIONS_KEY = "disable-user-extensions"
-
-# https://developer.chrome.com/extensions/nativeMessaging#native-messaging-host-protocol
-MESSAGE_LENGTH_SIZE = 4
-
-
-# https://wiki.gnome.org/Projects/GnomeShell/Extensions/UUIDGuidelines
-def is_uuid(uuid):
- return uuid is not None and re.match('[-a-zA-Z0-9@._]+$', uuid) is not None
-
-
-def debug(message):
- if DEBUG_ENABLED:
- log_error(message)
-
-
-def log_error(message):
- print('[%d] %s' % (os.getpid(), message), file=sys.stderr)
-
-
-class ChromeGNOMEShell(Gio.Application):
- def __init__(self, run_as_service):
- Gio.Application.__init__(
- self,
- application_id='org.gnome.ChromeGnomeShell',
- flags=Gio.ApplicationFlags.IS_SERVICE if run_as_service
- else Gio.ApplicationFlags.IS_LAUNCHER | Gio.ApplicationFlags.HANDLES_OPEN
- )
-
- self.gio_settings = None
- self.shellAppearedId = None
- self.shellSignalId = None
- self.disableUserExtensionsSignalId = None
- self.disableVersionCheckSignalId = None
-
- # Set custom exception hook
- # noinspection SpellCheckingInspection
- sys.excepthook = self.default_exception_hook
-
- self.register()
-
- if not run_as_service:
- self.shell_proxy = Gio.DBusProxy.new_sync(self.get_dbus_connection(),
- Gio.DBusProxyFlags.NONE,
- None,
- 'org.gnome.Shell',
- '/org/gnome/Shell',
- 'org.gnome.Shell.Extensions',
- None)
-
- self.get_dbus_connection().signal_subscribe(
- self.get_application_id(),
- self.get_application_id(),
- None,
- "/org/gnome/ChromeGnomeShell",
- None,
- Gio.DBusSignalFlags.NONE,
- self.on_dbus_signal,
- None
- )
-
- stdin = GLib.IOChannel.unix_new(sys.stdin.fileno())
- stdin.set_encoding(None)
- stdin.set_buffered(False)
-
- GLib.io_add_watch(stdin, GLib.PRIORITY_DEFAULT, GLib.IOCondition.IN, self.on_input, None)
- GLib.io_add_watch(stdin, GLib.PRIORITY_DEFAULT, GLib.IOCondition.HUP, self.on_hup, None)
- GLib.io_add_watch(stdin, GLib.PRIORITY_DEFAULT, GLib.IOCondition.ERR, self.on_hup, None)
- else:
- self.add_simple_action("create-notification", self.on_create_notification, 'a{sv}')
- self.add_simple_action("on-notification-clicked", self.on_notification_clicked, 's')
- self.add_simple_action("on-notification-action", self.on_notification_action, '(si)')
-
- GLib.timeout_add_seconds(5 * 60, self.on_service_timeout, None)
-
- GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.on_sigint, None)
-
- if not run_as_service or not self.get_is_remote():
- self.hold()
-
- # Is there any way to hook this to shutdown?
- def clean_release(self):
- debug('Release')
-
- if self.shellAppearedId:
- Gio.bus_unwatch_name(self.shellAppearedId)
-
- if self.shellSignalId:
- dbus_connection = self.get_dbus_connection()
-
- if dbus_connection is not None:
- dbus_connection.signal_unsubscribe(self.shellSignalId)
-
- if self.disableUserExtensionsSignalId:
- if self.gio_settings is not None:
- self.gio_settings.disconnect(self.disableUserExtensionsSignalId)
-
- if self.disableVersionCheckSignalId:
- if self.gio_settings is not None:
- self.gio_settings.disconnect(self.disableVersionCheckSignalId)
-
- self.release()
-
- def default_exception_hook(self, exception_type, value, tb):
- log_error("Uncaught exception of type %s occured" % exception_type)
- traceback.print_tb(tb)
- log_error("Exception: %s" % value)
-
- self.clean_release()
-
- def add_simple_action(self, name, callback, parameter_type):
- action = Gio.SimpleAction.new(
- name,
- GLib.VariantType.new(parameter_type) if parameter_type is not None else None
- )
- action.connect('activate', callback)
- self.add_action(action)
-
- # Service events
- # noinspection PyUnusedLocal
- def on_create_notification(self, source, request):
- debug('On create notification')
-
- request = request.unpack()
-
- notification = Gio.Notification.new(request['title'])
- notification.set_body(request['message'])
- notification.set_priority(Gio.NotificationPriority.NORMAL)
- notification.set_default_action_and_target(
- "app.on-notification-clicked",
- GLib.Variant.new_string(request['name'])
- )
-
- if 'buttons' in request:
- for button_id, button in enumerate(request['buttons']):
- notification.add_button_with_target(
- button['title'],
- "app.on-notification-action",
- GLib.Variant.new_tuple(
- GLib.Variant.new_string(request['name']),
- GLib.Variant.new_int32(button_id)
- )
- )
-
- self.send_notification(request['name'], notification)
-
- # noinspection PyUnusedLocal
- def on_notification_action(self, notification, parameters):
- debug('Notification %s action: %s' % parameters.unpack())
-
- self.get_dbus_connection().emit_signal(
- None,
- self.get_dbus_object_path(),
- self.get_application_id(),
- "NotificationAction",
- parameters
- )
-
- # noinspection PyUnusedLocal
- def on_notification_clicked(self, notification, notification_name):
- debug('Notification %s clicked' % notification_name)
-
- self.get_dbus_connection().emit_signal(
- None,
- self.get_dbus_object_path(),
- self.get_application_id(),
- "NotificationClicked",
- GLib.Variant.new_tuple(notification_name)
- )
-
- # noinspection PyUnusedLocal
- def on_service_timeout(self, data):
- debug('On service timeout')
- self.clean_release()
-
- return False
-
- # Native messaging events
- # noinspection PyUnusedLocal
- def on_input(self, source, condition, data):
- debug('On input')
- text_length_bytes = source.read(MESSAGE_LENGTH_SIZE)
-
- if len(text_length_bytes) == 0:
- debug('Release condition: %s' % str(condition))
- self.clean_release()
- return
-
- # Unpack message length as 4 byte integer.
- text_length = struct.unpack(b'i', text_length_bytes)[0]
-
- # Read the text (JSON object) of the message.
- text = source.read(text_length).decode('utf-8')
-
- request = json.loads(text)
-
- if 'execute' in request:
- if 'uuid' in request and not is_uuid(request['uuid']):
- return
-
- self.process_request(request)
-
- return True
-
- # noinspection SpellCheckingInspection,PyUnusedLocal
- def on_dbus_signal(self, connection, sender_name, object_path, interface_name, signal_name, parameters, user_data):
- debug('Signal %s from %s' % (signal_name, interface_name))
-
- if interface_name == "org.gnome.Shell.Extensions" and signal_name == 'ExtensionStatusChanged':
- self.send_message({'signal': signal_name, 'parameters': parameters.unpack()})
- elif interface_name == self.get_application_id():
- if signal_name == 'NotificationAction':
- notification_name, button_id = parameters.unpack()
-
- self.send_message({
- 'signal': "NotificationAction",
- 'name': notification_name,
- 'button_id': button_id
- })
- elif signal_name == 'NotificationClicked':
- (notification_name,) = parameters.unpack()
-
- self.send_message({
- 'signal': "NotificationClicked",
- 'name': notification_name
- })
-
- # noinspection PyUnusedLocal
- def on_shell_appeared(self, connection, name, name_owner):
- debug('Signal: to %s' % name)
- self.send_message({'signal': name})
- debug('Signal: from %s' % name)
-
- def on_setting_changed(self, settings, key):
- if not key in (DISABLE_USER_EXTENSIONS_KEY, EXTENSION_DISABLE_VERSION_CHECK_KEY):
- return
-
- debug('on_setting_changed: %s=%s' % (key, settings.get_value(key).unpack()))
- self.send_message({
- 'signal': 'ShellSettingsChanged',
- 'key': key,
- 'value': settings.get_value(key).unpack()
- })
-
- # General events
- # noinspection PyUnusedLocal
- def on_hup(self, source, condition, data):
- debug('On hup: %s' % str(condition))
- self.clean_release()
-
- return False
-
- # noinspection PyUnusedLocal
- def on_sigint(self, data):
- debug('On sigint')
- self.clean_release()
-
- return False
-
- # Helpers
- # noinspection SpellCheckingInspection
- def dbus_call_response(self, method, parameters, result_property):
- try:
- result = self.shell_proxy.call_sync(method,
- parameters,
- Gio.DBusCallFlags.NONE,
- -1,
- None)
-
- self.send_message({'success': True, result_property: result.unpack()[0]})
- except GLib.GError as e:
- self.send_error(e.message)
-
- def send_error(self, message):
- self.send_message({'success': False, 'message': message})
-
- @staticmethod
- def send_message(response):
- """
- Helper function that sends a message to the webapp.
- :param response: dictionary of response data
- :return: None
- """
-
- message = json.dumps(response)
- message_length = len(message.encode('utf-8'))
-
- if message_length > 1024*1024:
- log_error('Too long message (%d): "%s"' % (message_length, message))
- return
-
- try:
- stdout = GLib.IOChannel.unix_new(sys.stdout.fileno())
- stdout.set_encoding(None)
- stdout.set_buffered(False)
-
- stdout.write_chars(struct.pack(b'I', message_length), MESSAGE_LENGTH_SIZE)
-
- # Write the message itself.
- stdout.write_chars(message.encode('utf-8'), message_length)
- except IOError as e:
- log_error('IOError occured: %s' % e.strerror)
- sys.exit(1)
-
- def get_variant(self, data, basic_type=False):
- if isinstance(data, ("".__class__, u"".__class__)) or type(data) is int or basic_type:
- if isinstance(data, ("".__class__, u"".__class__)):
- return GLib.Variant.new_string(data)
- elif type(data) is int:
- return GLib.Variant.new_int32(data)
- else:
- raise Exception("Unknown basic data type: %s, %s" % (type(data), str(data)))
- elif type(data) is list:
- variant_builder = GLib.VariantBuilder.new(GLib.VariantType.new('av'))
-
- for value in data:
- variant_builder.add_value(GLib.Variant.new_variant(self.get_variant(value)))
-
- return variant_builder.end()
-
- elif type(data) is dict:
- variant_builder = GLib.VariantBuilder.new(GLib.VariantType.new('a{sv}'))
-
- for key in data:
- if data[key] is None:
- continue
-
- if sys.version < '3':
- # pylint: disable=E0602
- # noinspection PyUnresolvedReferences
- key_string = unicode(key)
- else:
- key_string = str(key)
-
- variant_builder.add_value(
- GLib.Variant.new_dict_entry(
- self.get_variant(key_string, True), GLib.Variant.new_variant(self.get_variant(data[key]))
- )
- )
-
- return variant_builder.end()
- else:
- raise Exception("Unknown data type: %s" % type(data))
-
- def obtain_gio_settings(self):
- if not self.gio_settings:
- source = Gio.SettingsSchemaSource.get_default()
-
- if source is not None and source.lookup(SHELL_SCHEMA, True) is not None:
- self.gio_settings = Gio.Settings.new(SHELL_SCHEMA)
-
- def set_shell_boolean(self, key, value):
- self.obtain_gio_settings()
- if key in self.gio_settings.keys():
- return self.gio_settings.set_boolean(key, True if value else False)
-
- return False
-
- def process_request(self, request):
- debug('Execute: to %s' % request['execute'])
-
- if request['execute'] == 'initialize':
- shell_version = self.shell_proxy.get_cached_property("ShellVersion")
-
- if shell_version is not None:
- self.obtain_gio_settings()
-
- if EXTENSION_DISABLE_VERSION_CHECK_KEY in self.gio_settings.keys():
- disable_version_check = self.gio_settings.get_boolean(EXTENSION_DISABLE_VERSION_CHECK_KEY)
- else:
- disable_version_check = False
-
- if DISABLE_USER_EXTENSIONS_KEY in self.gio_settings.keys():
- disable_user_extensions = self.gio_settings.get_boolean(DISABLE_USER_EXTENSIONS_KEY)
- else:
- disable_user_extensions = False
-
- supports = ['notifications', 'v6']
-
- self.send_message(
- {
- 'success': True,
- 'properties': {
- 'connectorVersion': CONNECTOR_VERSION,
- 'shellVersion': shell_version.unpack(),
- 'versionValidationEnabled': not disable_version_check,
- 'userExtensionsDisabled': disable_user_extensions,
- 'supports': supports
- }
- }
- )
- else:
- self.send_message(
- {
- 'success': False,
- 'message': "no_gnome_shell"
- }
- )
-
- elif request['execute'] == 'subscribeSignals':
- if not self.shellAppearedId:
- self.shellAppearedId = Gio.bus_watch_name_on_connection(
- self.get_dbus_connection(),
- 'org.gnome.Shell',
- Gio.BusNameWatcherFlags.NONE,
- self.on_shell_appeared,
- None
- )
-
- if not self.shellSignalId:
- self.shellSignalId = self.get_dbus_connection().signal_subscribe(
- "org.gnome.Shell",
- "org.gnome.Shell.Extensions",
- "ExtensionStatusChanged",
- "/org/gnome/Shell",
- None,
- Gio.DBusSignalFlags.NONE,
- self.on_dbus_signal,
- None
- )
-
- if not self.disableUserExtensionsSignalId:
- self.obtain_gio_settings()
- self.disableUserExtensionsSignalId = self.gio_settings.connect(
- "changed::%s" % DISABLE_USER_EXTENSIONS_KEY,
- self.on_setting_changed)
-
- if not self.disableVersionCheckSignalId:
- self.obtain_gio_settings()
- self.disableVersionCheckSignalId = self.gio_settings.connect(
- "changed::%s" % EXTENSION_DISABLE_VERSION_CHECK_KEY,
- self.on_setting_changed)
-
- elif request['execute'] == 'installExtension':
- self.dbus_call_response(
- "InstallRemoteExtension",
- GLib.Variant.new_tuple(GLib.Variant.new_string(request['uuid'])),
- "status"
- )
-
- elif request['execute'] == 'listExtensions':
- self.dbus_call_response("ListExtensions", None, "extensions")
-
- elif request['execute'] == 'enableExtension':
- self.obtain_gio_settings()
- uuids = self.gio_settings.get_strv(ENABLED_EXTENSIONS_KEY)
-
- extensions = []
- if 'extensions' in request:
- extensions = request['extensions']
- else:
- extensions.append({'uuid': request['uuid'], 'enable': request['enable']})
-
- for extension in extensions:
- if not is_uuid(extension['uuid']):
- continue
-
- if extension['enable']:
- if not extension['uuid'] in uuids:
- uuids.append(extension['uuid'])
- elif extension['uuid'] in uuids:
- uuids = [value for value in uuids if value != extension['uuid']]
-
- self.gio_settings.set_strv(ENABLED_EXTENSIONS_KEY, uuids)
-
- self.send_message({'success': True})
-
- elif request['execute'] == 'launchExtensionPrefs':
- self.shell_proxy.call("LaunchExtensionPrefs",
- GLib.Variant.new_tuple(GLib.Variant.new_string(request['uuid'])),
- Gio.DBusCallFlags.NONE,
- -1,
- None,
- None,
- None)
-
- elif request['execute'] == 'getExtensionErrors':
- self.dbus_call_response("GetExtensionErrors",
- GLib.Variant.new_tuple(GLib.Variant.new_string(request['uuid'])),
- "extensionErrors")
-
- elif request['execute'] == 'getExtensionInfo':
- self.dbus_call_response("GetExtensionInfo",
- GLib.Variant.new_tuple(GLib.Variant.new_string(request['uuid'])),
- "extensionInfo")
-
- elif request['execute'] == 'uninstallExtension':
- self.dbus_call_response("UninstallExtension",
- GLib.Variant.new_tuple(GLib.Variant.new_string(request['uuid'])),
- "status")
-
- elif request['execute'] == 'setUserExtensionsDisabled':
- self.send_message({
- 'success': self.set_shell_boolean(
- DISABLE_USER_EXTENSIONS_KEY,
- request['disable']
- )
- })
-
- elif request['execute'] == 'setVersionValidationDisabled':
- self.send_message({
- 'success': self.set_shell_boolean(
- EXTENSION_DISABLE_VERSION_CHECK_KEY,
- request['disable']
- )
- })
-
- elif request['execute'] == 'createNotification':
- Gio.DBusActionGroup.get(
- app.get_dbus_connection(),
- app.get_application_id(),
- app.get_dbus_object_path()
- ).activate_action('create-notification', self.get_variant({
- 'name': request['name'],
- 'title': request['options']['title'],
- 'message': request['options']['message'],
- 'buttons': request['options']['buttons']
- }))
-
- elif request['execute'] == 'removeNotification':
- self.withdraw_notification(request['name'])
-
- debug('Execute: from %s' % request['execute'])
-
-
-if __name__ == '__main__':
- debug('Main. Use Ctrl+D to quit.')
-
- run_as_service = False
- if '--gapplication-service' in sys.argv:
- run_as_service = True
- sys.argv.remove('--gapplication-service')
-
- app = ChromeGNOMEShell(run_as_service)
- app.run(sys.argv)
-
- debug('Quit')
diff --git a/connector/org.gnome.ChromeGnomeShell.desktop b/connector/org.gnome.ChromeGnomeShell.desktop
deleted file mode 100644
index 2eb4ab520fc6546ef6afd7274ed908f616913912..0000000000000000000000000000000000000000
--- a/connector/org.gnome.ChromeGnomeShell.desktop
+++ /dev/null
@@ -1,8 +0,0 @@
-[Desktop Entry]
-Type=Application
-Encoding=UTF-8
-Name=GNOME Shell integration
-Comment=Provides integration with GNOME Shell and the corresponding extensions repository https://extensions.gnome.org.
-Icon=org.gnome.ChromeGnomeShell
-DBusActivatable=true
-NoDisplay=true
diff --git a/connector/org.gnome.ChromeGnomeShell.service.in b/connector/org.gnome.ChromeGnomeShell.service.in
deleted file mode 100644
index f22fec4bbe2b124cfad576b4e19cb1225dc4c298..0000000000000000000000000000000000000000
--- a/connector/org.gnome.ChromeGnomeShell.service.in
+++ /dev/null
@@ -1,3 +0,0 @@
-[D-BUS Service]
-Name=org.gnome.ChromeGnomeShell
-Exec=${CMAKE_INSTALL_FULL_BINDIR}/chrome-gnome-shell --gapplication-service
diff --git a/connector/org.gnome.chrome_gnome_shell.firefox.json b/connector/org.gnome.chrome_gnome_shell.firefox.json
deleted file mode 100644
index d93ca735d909c032bde626e2c4fa0d627755cdcf..0000000000000000000000000000000000000000
--- a/connector/org.gnome.chrome_gnome_shell.firefox.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "allowed_extensions": [ "chrome-gnome-shell@gnome.org" ]
-}
diff --git a/connector/org.gnome.chrome_gnome_shell.json b/connector/org.gnome.chrome_gnome_shell.json
deleted file mode 100644
index 3d94d5cc42b6eb7829350505ba40219640ebddd2..0000000000000000000000000000000000000000
--- a/connector/org.gnome.chrome_gnome_shell.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "org.gnome.chrome_gnome_shell",
- "description": "Native connector for extensions.gnome.org",
- "path": "${CMAKE_INSTALL_FULL_BINDIR}/chrome-gnome-shell",
- "type": "stdio",
- "allowed_origins": [
- "chrome-extension://${CHROME_EXTENSION_ID}/",
- "chrome-extension://${OPERA_EXTENSION_ID}/"
- ]
-}
diff --git a/connector/setup.py b/connector/setup.py
deleted file mode 100755
index ee68bdb88caf9c16acf00db573da5d0f683c3707..0000000000000000000000000000000000000000
--- a/connector/setup.py
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env python
-
-import os
-import shutil
-from distutils.core import setup
-
-SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
-BUILD_DIR = SCRIPT_DIR + '/../build'
-SCRIPT_PATH = BUILD_DIR + '/chrome-gnome-shell'
-
-if not os.path.exists(BUILD_DIR):
- os.makedirs(BUILD_DIR)
-shutil.copyfile(SCRIPT_DIR + '/chrome-gnome-shell.py', SCRIPT_PATH)
-
-setup(
- name='chrome-gnome-shell',
- description='Provides integration with GNOME Shell extensions repository for Chrome browser',
- author='Yuri Konotopov',
- url='https://wiki.gnome.org/Projects/GnomeShellIntegrationForChrome',
- scripts=[SCRIPT_PATH]
-)
diff --git a/contrib/cp.py b/contrib/cp.py
new file mode 100755
index 0000000000000000000000000000000000000000..8de9ac8f10dd60d11626c86e926f9c95d2eb8f7d
--- /dev/null
+++ b/contrib/cp.py
@@ -0,0 +1,50 @@
+#!/usr/bin/python3
+
+import argparse
+import os
+import shutil
+import sys
+
+
+def main():
+ parser = argparse.ArgumentParser(os.path.basename(sys.argv[0]))
+ parser.add_argument(
+ "--ignore",
+ help="Ignore files when copying directory"
+ )
+ parser.add_argument(
+ "--rename",
+ help="Rename file"
+ )
+ parser.add_argument(
+ "src",
+ help="Source"
+ )
+ parser.add_argument(
+ "dest",
+ help="Destination"
+ )
+ args = parser.parse_args()
+
+ os.makedirs(args.dest, exist_ok=True)
+
+ if os.path.isdir(args.src):
+ shutil.copytree(
+ args.src,
+ args.dest,
+ ignore=shutil.ignore_patterns(args.ignore),
+ dirs_exist_ok=True
+ )
+ else:
+ shutil.copy(
+ args.src,
+ (
+ args.dest
+ if not args.rename
+ else os.path.join(args.dest, args.rename)
+ )
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/contrib/merge_json.py b/contrib/merge_json.py
new file mode 100755
index 0000000000000000000000000000000000000000..870f07c794d04cffcf91c48bc884334580eae2c0
--- /dev/null
+++ b/contrib/merge_json.py
@@ -0,0 +1,43 @@
+#!/usr/bin/python3
+
+import argparse
+import json
+import os
+import sys
+
+
+def main():
+ parser = argparse.ArgumentParser(os.path.basename(sys.argv[0]))
+ parser.add_argument(
+ "--delete",
+ default='',
+ help="Remove keys from json, comma separated"
+ )
+ parser.add_argument(
+ "--output",
+ required=True,
+ help="Write output to file"
+ )
+ parser.add_argument(
+ "input",
+ nargs='+',
+ help="Chrome extension key"
+ )
+ args = parser.parse_args()
+
+ output = {}
+ for file in args.input:
+ with open(file, 'r') as fp:
+ data = json.load(fp)
+ for key in args.delete.split(','):
+ if key in data:
+ del data[key]
+
+ output = {**output, **data}
+
+ with open(args.output, 'w') as fp:
+ json.dump(output, fp, ensure_ascii=False, indent=2)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..c331b65fc88637a8e689601106511d99fbcfb0fe
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,159 @@
+project('gnome-browser-extension',
+ license: 'GPL-3',
+ version : '10.1',
+)
+
+# Constants
+CONTRIB_PATH = meson.global_source_root() / 'contrib'
+EXTENSION_PATH = meson.global_source_root() / 'extension'
+PO_PATH = meson.global_source_root() / 'po'
+
+EXTENSION_BUILD_DIR = meson.current_build_dir() / 'extension'
+CHROME_BUILD_DIR = EXTENSION_BUILD_DIR / 'chrome'
+FIREFOX_BUILD_DIR = EXTENSION_BUILD_DIR / 'firefox'
+
+# Copy helper
+cp = CONTRIB_PATH / 'cp.py'
+
+# Filter that excludes manifest templates
+extension_exclude = 'manifest*.json'
+
+# Build requirements
+msgmerge = find_program('msgmerge', required: get_option('build_messages'))
+sevenz = find_program('7z', required: get_option('build_source_package'))
+
+if get_option('build_messages')
+ py_modules = ['polib']
+else
+ py_modules = []
+endif
+
+py = import('python')
+py.find_installation('python3', required: true, modules: py_modules)
+
+# Chrome Store public extension key
+chrome_key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlig8TAPPQZinMkJnptC0ldizx6fG9jSjZDJ9c8GuLcXeGRH+NMlQuPC9bR5IQlT7+4VY/1tm1+IZ4xvITx1wXCNTR+KXzZv3VNc2D+logehK7oIRTRj0fLhixrx4NLSNK7L7HgV2xcIoW6QV0jOdFcTPL0mWXodXSzZePrvXuflF7qpwNxLzYVi04Vh3xu2oR2Pc9SwfZ4SNbyCaunH/p8n5AYmDuogI2Ah++RZw0ctnqn7mmHrGXteBu/vkpcHZu3B3eW9PFSrv69rRs8duybYR9C91hJm6yzRqZqIpindIU3k2HnNWeCFWkRVpZPhaNVoxcBUO7wWUUwdIflW2JwIDAQAB'
+
+# Simple python helper that enumerates translations
+lang_glob = '''
+import glob
+import os
+
+os.chdir(os.path.join(os.getenv("MESON_SOURCE_ROOT"), "po"))
+print("\n".join([l.split('.')[0] for l in glob.glob("*.po")]), end='')
+'''
+
+if get_option('build_extension')
+ conf_data = configuration_data()
+ conf_data.set('PUBLIC_KEY', chrome_key)
+
+ # Prepare Chrome manifest
+ configure_file(
+ input: EXTENSION_PATH / 'manifest.json',
+ output: 'manifest-chrome.json',
+ configuration: conf_data,
+ format: 'cmake',
+ )
+
+ # Prepare Firefox manifest
+ configure_file(
+ input: [
+ EXTENSION_PATH / 'manifest.json',
+ EXTENSION_PATH / 'manifest.firefox.json',
+ ],
+ output: 'manifest-firefox.json',
+ command: [
+ CONTRIB_PATH / 'merge_json.py',
+ '--delete', 'key',
+ '--output', '@OUTPUT@',
+ '@INPUT@'
+ ],
+ )
+
+ # Build Chrome extension
+ run_command(
+ cp,
+ '--rename', 'manifest.json',
+ meson.current_build_dir() / 'manifest-chrome.json',
+ CHROME_BUILD_DIR,
+ check: true,
+ )
+ run_command(
+ cp,
+ '--ignore', extension_exclude,
+ EXTENSION_PATH,
+ CHROME_BUILD_DIR,
+ check: true,
+ )
+ custom_target('chrome-zip',
+ command: [
+ sevenz, 'a', '-tzip',
+ meson.current_build_dir() / 'extension-chrome.zip',
+ CHROME_BUILD_DIR / '*',
+ ],
+ output: 'extension-chrome.zip',
+ build_by_default: true,
+ )
+
+ # Build Firefox extension
+ run_command(
+ cp,
+ '--rename', 'manifest.json',
+ meson.current_build_dir() / 'manifest-firefox.json',
+ FIREFOX_BUILD_DIR,
+ check: true,
+ )
+ run_command(
+ cp,
+ '--ignore', extension_exclude,
+ EXTENSION_PATH,
+ FIREFOX_BUILD_DIR,
+ check: true,
+ )
+ custom_target('firefox-zip',
+ command: [
+ sevenz, 'a', '-tzip',
+ meson.current_build_dir() / 'extension-firefox.zip',
+ FIREFOX_BUILD_DIR / '*',
+ ],
+ output: 'extension-firefox.zip',
+ build_by_default: true,
+ )
+endif
+
+if get_option('build_messages')
+ messages_deps = []
+
+ # Update gettext's pot template from Chrome's messages.json
+ run_command(
+ meson.global_source_root() / 'contrib/chrome-messages2po.py',
+ '--email', get_option('gettext_report_email'),
+ '--reference-lang', 'en',
+ '--write-pot',
+ meson.global_source_root() / 'contrib/chrome-web-store/',
+ meson.global_source_root() / 'extension/_locales/',
+ meson.global_source_root() / 'po/',
+ check: true,
+ )
+
+ # Update *.po from generated template
+ langs = run_command('python3', '-c', lang_glob, check: true).stdout().split('\n')
+ foreach lang : langs
+ run_command(
+ msgmerge, '-q', '--update',
+ PO_PATH / lang + '.po',
+ PO_PATH / 'chrome-gnome-shell.pot',
+ check: true,
+ )
+ endforeach
+
+ # Regenerate extension locales
+ run_command(
+ CONTRIB_PATH / 'po2chrome-messages.py',
+ '--reference-lang', 'en',
+ CONTRIB_PATH / 'chrome-web-store/',
+ meson.global_source_root() / 'extension' / '_locales/',
+ PO_PATH,
+ check: true,
+ )
+endif
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000000000000000000000000000000000000..43ed6ae2e7c83a8ce6035c4a7e59f7327e22b6a7
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,10 @@
+option('build_extension', type: 'boolean', value: true, description: 'Build extension zip package')
+option('build_messages', type: 'boolean', value: false, description: 'Update translation strings')
+option('build_source_package', type: 'boolean', value: false, description: 'Build source package')
+
+option(
+ 'gettext_report_email',
+ type: 'string',
+ value: 'ykonotopov@gnome.org',
+ description : 'Report-Msgid-Bugs-To field value in pot file'
+)