Setting up SUSI Desktop Locally for Development and Using Webview Tag and Adding Event Listeners

SUSI Desktop is a cross platform desktop application based on electron which presently uses chat.susi.ai as a submodule and allows the users to interact with susi right from their desktop.

Any electron app essentially comprises of the following components

    • Main Process (Managing windows and other interactions with the operating system)
    • Renderer Process (Manage the view inside the BrowserWindow)

Steps to setup development environment

      • Clone the repo locally.
$ git clone https://github.com/fossasia/susi_desktop.git
$ cd susi_desktop
      • Install the dependencies listed in package.json file.
$ npm install
      • Start the app using the start script.
$ npm start

Structure of the project

The project was restructured to ensure that the working environment of the Main and Renderer processes are separate which makes the codebase easier to read and debug, this is how the current project is structured.

The root directory of the project contains another directory ‘app’ which contains our electron application. Then we have a package.json which contains the information about the project and the modules required for building the project and then there are other github helper files.

Inside the app directory-

  • Main – Files for managing the main process of the app
  • Renderer – Files for managing the renderer process of the app
  • Resources – Icons for the app and the tray/media files
  • Webview Tag

    Display external web content in an isolated frame and process, this is used to load chat.susi.ai in a BrowserWindow as

    <webview src="https://chat.susi.ai/"></webview>
    

    Adding event listeners to the app

    Various electron APIs were used to give a native feel to the application.

  • Send focus to the window WebContents on focussing the app window.
  • win.on('focus', () => {
    	win.webContents.send('focus');
    });
    
  • Display the window only once the DOM has completely loaded.
  • const page = mainWindow.webContents;
    ...
    page.on('dom-ready', () => {
    	mainWindow.show();
    });
    
  • Display the window on ‘ready-to-show’ event
  • win.once('ready-to-show', () => {
    	win.show();
    });
    

    Resources

    1. A quick article to understand electron’s main and renderer process by Cameron Nokes at Medium link
    2. Official documentation about the webview tag at https://electron.atom.io/docs/api/webview-tag/
    3. Read more about electron processes at https://electronjs.org/docs/glossary#process
    4. SUSI Desktop repository at https://github.com/fossasia/susi_desktop.

    Enhancing SUSI Desktop to Display a Loading Animation and Auto-Hide Menu Bar by Default

    SUSI Desktop is a cross platform desktop application based on electron which presently uses chat.susi.ai as a submodule and allows the users to interact with susi right from their desktop. The benefits of using chat.susi.ai as a submodule is that it inherits all the features that the webapp offers and thus serves them in a nicely build native application.

    Display a loading animation during DOM load.

    Electron apps should give a native feel, rather than feeling like they are just rendering some DOM, it would be great if we display a loading animation while the web content is actually loading, as depicted in the gif below is how I implemented that.
    Electron provides a nice, easy to use API for handling BrowserWindow, WebContent events. I read through the official docs and came up with a simple solution for this, as depicted in the below snippet.

    onload = function () {
    	const webview = document.querySelector('webview');
    	const loading = document.querySelector('#loading');
    
    	function onStopLoad() {
    		loading.classList.add('hide');
    	}
    
    	function onStartLoad() {
    		loading.classList.remove('hide');
    	}
    
    	webview.addEventListener('did-stop-loading', onStopLoad);
    	webview.addEventListener('did-start-loading', onStartLoad);
    };
    

    Hiding menu bar as default

    Menu bars are useful, but are annoying since they take up space in main window, so I hid them by default and users can toggle their display on pressing the Alt key at any point of time, I used the autoHideMenuBar property of BrowserWindow class while creating an object to achieve this.

    const win = new BrowserWindow({
    	
    	show: false,
    	autoHideMenuBar: true
    });
    

    Resources

    1. More information about BrowserWindow class in the official documentation at electron.atom.io.
    2. Follow a quick tutorial to kickstart creating apps with electron at https://www.youtube.com/watch?v=jKzBJAowmGg.
    3. SUSI Desktop repository at https://github.com/fossasia/susi_desktop.

    How to create a Windows Installer from tagged commits

    I working on an open-source Python project, an editor for knit work called the “KnitEditor”. Development takes place at Github. Here, I would like to give some insight in how we automated deployment of the application to a Windows installer.

    The process works like this:

    1. Create a tag with git and push it to Github.
    2. AppVeyor build the application.
    3. AppVeyor pushes the application to the Github release.

    (1) Create a tag and push it

    Tags should reflect the version of the software. Version “0.0.1” is in tag “v0.0.1”. We automated the tagging with the “setup.py” in the repository. Now, you can run

    py -3.4 setup.py tag_and_deploy
    

    Which checks that there is no such tag already. Several commits have the same version, so, we like to make sure that we do not have two versions with the same name. Also, this can only be executed on the master branch. This way, the software has gone through all the automated quality assurance. Here is the code from the setup.py:

    from distutils.core import Command
    # ...
    class TagAndDeployCommand(Command):
    
        description = "Create a git tag for this version and push it to origin."\
                      "To trigger a travis-ci build and and deploy."
        user_options = []
        name = "tag_and_deploy"
        remote = "origin"
        branch = "master"
    
        def initialize_options(self):
            pass
    
        def finalize_options(self):
            pass
    
        def run(self):
            if subprocess.call(["git", "--version"]) != 0:
                print("ERROR:\n\tPlease install git.")
                exit(1)
            status_lines = subprocess.check_output(
                ["git", "status"]).splitlines()
            current_branch = status_lines[0].strip().split()[-1].decode()
            print("On branch {}.".format(current_branch))
            if current_branch != self.branch:
                print("ERROR:\n\tNew tags can only be made from branch"
                      " \"{}\".".format(self.branch))
                print("\tYou can use \"git checkout {}\" to switch"
                      " the branch.".format(self.branch))
                exit(1)
            tags_output = subprocess.check_output(["git", "tag"])
            tags = [tag.strip().decode() for tag in tags_output.splitlines()]
            tag = "v" + __version__
            if tag in tags:
                print("Warning: \n\tTag {} already exists.".format(tag))
                print("\tEdit the version information in {}".format(
                        os.path.join(HERE, PACKAGE_NAME, "__init__.py")
                    ))
            else:
                print("Creating tag \"{}\".".format(tag))
                subprocess.check_call(["git", "tag", tag])
            print("Pushing tag \"{}\" to remote \"{}\"."
                  "".format(tag, self.remote))
            subprocess.check_call(["git", "push", self.remote, tag])
    # ...
    SETUPTOOLS_METADATA = dict(
    # ...
        cmdclass={
    # ...
            TagAndDeployCommand.name: TagAndDeployCommad
        },
    )
    # ...
    if __name__ == "__main__":
        import setuptools
        METADATA.update(SETUPTOOLS_METADATA)
        setuptools.setup(**METADATA) # METADATA can be found in several other 
    

    Above, you can see a “distutils” command that executed git through the command line interface.

    (2) AppVeyor builds the application

    As mentioned above, you can configure AppVeyor to build your application. Here are some parts of the “appveyor.yml” file, that I comment on inline:

    # see https://packaging.python.org/appveyor/#adding-appveyor-support-to-your-project
    environment:
      PYPI_USERNAME: niccokunzmann3
      PYPI_PASSWORD:
        secure: Gxrd9WI60wyczr9mHtiQHvJ45Oq0UyQZNrvUtKs2D5w=
    
      # For Python versions available on Appveyor, see
      # http://www.appveyor.com/docs/installed-software#python
      # The list here is complete (excluding Python 2.6, which
      # isn't covered by this document) at the time of writing.
    
      # we only need Python 3.4 for kivy
      PYTHON: "C:\\Python34"
    
    
    install:
      - "%PYTHON%\\python.exe -m pip install docutils pygments pypiwin32 kivy.deps.sdl2 kivy.deps.glew"
      - "%PYTHON%\\python.exe -m pip install -r requirements.txt"
      - "%PYTHON%\\python.exe -m pip install -r test-requirements.txt"
      - "%PYTHON%\\python.exe setup.py install"
      
    build_script:
    - cmd: cmd /c windows-build\build.bat
    
    test_script:
      # Put your test command here.
      # If you don't need to build C extensions on 64-bit Python 3.3 or 3.4,
      # you can remove "build.cmd" from the front of the command, as it's
      # only needed to support those cases.
      # Note that you must use the environment variable %PYTHON% to refer to
      # the interpreter you're using - Appveyor does not do anything special
      # to put the Python version you want to use on PATH.
      - windows-build\dist\KnitEditor\KnitEditor.exe /test
      - "%PYTHON%\\python.exe -m pytest --pep8 kniteditor"
    
    artifacts:
      # bdist_wheel puts your built wheel in the dist directory
    - path: dist/*
      name: distribution
    - path: windows-build/dist/Installer/KnitEditorInstaller.exe
      name: installer
    - path: windows-build/dist/KnitEditor
      name: standalone
    
    deploy:
    - provider: GitHub
      # http://www.appveyor.com/docs/deployment/github
      tag: $(APPVEYOR_REPO_TAG_NAME)
      description: "Release $(APPVEYOR_REPO_TAG_NAME)"
      auth_token:
        secure: j1EbCI55pgsetM/QyptIM/QDZC3SR1i4Xno6jjJt9MNQRHsBrFiod0dsuS9lpcC7
      artifact: installer
      force_update: true
      draft: false
      prerelease: false
      on:
        branch: master                 # release from master branch only
        appveyor_repo_tag: true        # deploy on tag push only
    
    

    Note that the line

      - windows-build\dist\KnitEditor\KnitEditor.exe /test
    

    executes the tests in the built application.

    These commands are executed to build the application and are executed by this step:

    build_script:
    - cmd: cmd /c windows-build\build.bat
    
    "%PYTHON%\python.exe" -m pip install pyinstaller
    

    The line above installs pyinstaller

    "%PYTHON%\python.exe" -m PyInstaller KnitEditor.spec
    

    The line above uses pyinstaller to create an executable from the specification.

    "Inno Setup 5\ISCC.exe" KnitEditor.iss
    

    The line above uses Inno Setup to create the Installer for the built application.

    (3) Deploy to Github

    As you can see in the “appveyor.yml” file, the resulting executable is listed as an artifact. Artifacts can be downloaded directly from appveyor or used to deploy. In this case, we use the github deploy, which can be customized via the UI of appveyor.

    - path: windows-build/dist/Installer/KnitEditorInstaller.exe
      name: installer
    deploy:
    - provider: GitHub
      # http://www.appveyor.com/docs/deployment/github
      tag: $(APPVEYOR_REPO_TAG_NAME)
      description: "Release $(APPVEYOR_REPO_TAG_NAME)"
      auth_token:
        secure: j1EbCI55pgsetM/QyptIM/QDZC3SR1i4Xno6jjJt9MNQRHsBrFiod0dsuS9lpcC7
      artifact: installer
      force_update: true
      draft: false
      prerelease: false
      on:
        branch: master                 # release from master branch only
        appveyor_repo_tag: true        # deploy on tag push only
    

    Summary

    Now, every time we push a tag to Github, AppVeyor build a new installer for our application.