If you don’t know what Electron is, it’s a wrapper of Chromium which allows you to develop desktop applications using Web technologies like HTML, CSS, and JavaScript. It’s been largely adopted by big companies in the last few years because it allows developers to use these very common technologies to develop desktop apps and because it provides multi-platform support out of the box. It works as an extension of Node.js and it’s open-sourced, meaning that anybody can use it.

Preview

This is what we’re going to be doing:

In this article, we’re going to talk about how to alter (as in hack, not contribute) apps created with Electron. Specifically how to restyle Skype and its ‘modern’ redesign.

There are a few big companies out there already using Electron. Here are a few examples:

  • Discord
  • Slack
  • Skype
  • WordPress

Before we move on, I’d like to note that this is just a very raw example of how to alter something to behave or look like you want. So don’t expect to see me using beautiful code or best practices.

How is this Possible?

You might be wondering if you’ve read that right. The answer is yes, I said ‘alter a desktop app’. Electron packages apps built with JS, HTML, and CSS. And, as you probably know, you can’t really do much about hiding the source code of those. Sure, you can minify and uglify them, but beyond that, you’re exposing your source code to the user. All we have to do is decompress some archives and edit some files.

Why do this?

In general, for fun, but in this specific case, because a lot of people have expressed their disappointment with the new Skype look and feel.

Prerequisites

You need to understand a few basic principles of HTML, CSS, and JS to be able to do this yourself. But, don’t worry, at the end of the article you’ll find a patch file that anybody can use.

Tools

We’re going to need 2 tools to be able to decompress the Slack app and beautify the JS just so we can make some sense of it.

We’re going to install the tools globally via NPM.

npm install -g asar js-beautify

Apart from these, we need a code editor. Make sure to use something light like Vim or Sublime Text that can handle large files. The main JS file that we’re going to edit is a compiled version of all the components, so it’s a big file.

How to Edit an Electron App

Electron recommends using a tool that we’ve just installed, called asar, to package your project’s files when you want to send it to the user. This compresses the file size, as well as hides the source code more.

These compressed files will use the asar extension. That’s how we’ll identify them.

How to Edit Skype

In Skype’s case, this file is called app.asar and it can be found under its installation directory.

In macOS it’s at: /Applications/Skype.app/Contents/Resources

In Windows it’s at: C:\Program Files (x86)\Microsoft\Skype for Desktop

In Linux, it can be installed in various locations. You can search for it with the following command:

find / -name app.asar

Use the location under a directory called Skype or similar.

Let’s open a Terminal instance and navigate to that location.

cd /Applications/Skype.app/Contents/Resources

Decompressing the Archive

To decompress the Asar archive, we use the installed tool:

asar extract app.asar app

This is going to extract the contents of the app.asar archive to a directory called app.

When we open the Skype app, it’s going to look for the app.asar archive or the app folder. If they’re both present, it’s going to use the archive. Therefore, in order to be able to use the edited files inside the new app folder, we’re going to rename app.asar into something else. I’ll call mine app.asar.bak.

mv app.asar app.asar.bak

Now, if we’re going to close and restart the Skype app, we’re going to be using the files under the app folder. We’re not going to notice a difference, because we haven’t modified anything yet, but that’s how it works.

Activating the Developer Tools

We can enable the Developer Tools inside an Electron app since it’s a Chromium instance. On Skype, we can do this by pressing CTRL + ALT + SHIFT + D (it’s CTRL on macOS too) to enable the debugging mode. Now press CMD + ALT + I (CTRL + ALT + I on Windows) to open DevTools. If it appeared, perfect. If not, press CTRL + ALT + SHIFT + D a couple more times and try again.

Note, if you close the app and restart it, you will have to press CTRL + ALT + SHIFT + D again to re-enable debugging mode.

We’re going to use the Developer Tools to find out what to edit.

Editing Colors for Various Components

Some apps use CSS from separate files and HTML identifiers like IDs and classes, but this is not the case for Skype. Skype uses React.js, which uses inline CSS to style components. Hence, in order to style Skype, we need to alter JS. However, this is not particularly easy, because the JS code is uglified and to edit it, we’re going to guess exactly what needs to be altered.

Before deciding to edit Skype, I wanted to use the dark version of the interface, but it was unusable since it used #000 as its background color, which is a bit much for my taste.

Before we go on with the edits, it’s worth mentioning that Skype has different predefined styles, and you need to alter the bits belonging to that specific style. To change this style, press CTRL + T or CMD + T anywhere inside the app. I use the first (from the left) option under the Dark version.

We need to do one more thing before starting to edit. We need to beautify the app/js/app.js file, to at least make some sense of what’s in there.

We can use the tool that we’ve installed earlier. But first, let’s rename app.js to app.min.js to keep a copy of the original file.

mv app/js/app.js app/js/app.min.js
js-beautify app/js/app.min.js -o app/js/app.js

This will give us a beautified app.js file to work with. And it’s in here that we’ll do most of our edits.

Finding a color

With the DevTools open, click the element picker button on the top left and select something that you’re interested in changing the color of. For me, the first thing was the black background. So, I selected the background and there was no color applied to it. I had to move up and down the HTML hierarchy to find a wrapper that used a background-color style attribute. I found one that included part of the conversation window and was using background-color: rgb(0, 0, 0);. I wasn’t hoping for much, but I searched for rgb(0, 0, 0) and rgb(0,0,0) inside the app.js file. I knew that react translates CSS properties, so I searched for #000. Which is basically the same thing, if you know your CSS.

I found that as a value to an object property:

Remember, we’re hacking. We’re blindly trying things out, we can’t know for sure what’s going on when we’re editing minified code.

So, I edit that value and press CMD + R (CTRL + R in Windows) on the Skype window to refresh the app. CMR + R is a shortcut used by browsers to refresh a page, and since Skype is working inside a Chromium instance, it will use some of the same shortcuts. But this only works when debugging mode is enabled.

The window was refreshed, I selected a conversation and that confirmed that my edit was applied. Because I used a bright red to make sure I don’t get false positives.

Now that I got the variable, I was going to use a better color, but I didn’t have any idea of what exactly. So I copied the color above black, in the previous image, gray400.

Note that I was changing the color of a variable, which meant that that “black” variable was used in some other places. But I didn’t bother to look where that was going to be used, because I dislike the pure #000 anyway, so I was going to take a chance to change it everywhere.

That color turned out to be the same one that is used for the message text area, which made the interface immediately feel significantly better so I left it like that.

Because for some reason the color picked inside DevTools wasn’t working to change colors, I needed to match colors a different way, so I decided to only use black with different opacity levels, with rgba().

I searched for the block with the messages sent by me in DevTools and noticed again a rgb() color, but I needed the HEX version to search for it inside the app.js file. In DevTools, if you click to open the color picker of a color, you have the option to change the type used to display its code color. You can switch between RGB, RGBA, HSB and HEX. I switched to HEX and searched for that code.

The code for my messages’ background color was #2B333B, so I replaced that with rgba(0, 0, 0, .1), refreshed to see how it looked and liked it. I wanted to keep the other persons’ messages background colors different, so I changed that to rgba(0, 0, 0, .2). For the thumbnail previews inside the conversations, I used rgba(0, 0, 0, .15) just to be a little bit different. For the sidebar background, I also used the same gray as the one applied now to the conversation background, #2B2C33. I changed a few more colors to my liking.

Keeping Track of Your Changes

I knew that if Skype would release an update, I’d have to do all of this all over again, so I kept track of my changes in a new JSON file which I called updates.json:

{
    "js/app.js": {
        "replace": {
            "/black:\"#000000\"/i": "black:\"#2B2C33\"",
            "/#1E2224/gi": "rgba(0, 0, 0, .2)",
            "/#2B333B/gi": "rgba(0, 0, 0, .1)",
            "/#1F1F1F/gi": "#2B2C33",
            "/cardColor:i\\(6\\).color.gray400/gi": "cardColor:\"rgba(0, 0, 0, .15)\"",
            "/messageComposerBackgroundColor:i\\(6\\).color.gray400/gi": "messageComposerBackgroundColor:\"rgba(0,0,0,0)\"",
            "/,tiny:760,small:896/": ",tiny:0,small:0",
            "/,headerSizeSmall:24/g": ",headerSizeSmall:18"
        }
    }
}

Later, we’ll write a script to parse that file and replace the colors automatically.

Note, we’re replacing the stuff in the minified app.js file, because there’s less room for error, compared to beautifying and then replacing. This is going to be done automatically, so we don’t really care where we do the replace.

The line { "/,tiny:760,small:896/": ",tiny:0,small:0" }, is there to redefine the breaking points, this is used to disable the sidebar hiding under 760 pixels width and the media previews under 896 pixels width. Not that will probably break other functionality, but since I only use Skype at one dimension (around 600px width and full height), and it worked fine in my testing, I’m going to do it like that. If you want to simply disable the toggling of the sidebar, you might have to do a bit more digging to see exactly where that happens.

Those are all the style updates that I could do from the app.js file.

The rest, I tried to do directly from CSS. I wrote all my CSS inside the css/skype.css file.

That file is used to reset basic elements like html, body, a, h1, h2, h3, and so on.

Because I disabled toggling the sidebar at smaller dimensions, I needed to do something about it, because it was too big. I use my Skype window at around 600 px width and the sidebar has a width property of 322px, so it was more than half. I decided to hide everything in the sidebar, except for the profile pictures, and toggle everything back in when I hover over it.

Because Skype doesn’t use IDs and classes in its HTML, I had to improvise the selectors and I ended up with the following:

@media (max-width: 760px) {
  body {
    padding-left: 54px;
  }

  [style *= "width: 322px;"][style *= "position: relative;"] {
    background-color: #24252b !important;
    position: fixed !important;
    top: 0;
    left: 0;
    bottom: 0;
    width: 54px !important;
    transition: width .2s;
    border: none !important;
    z-index: 2;
  }

  /**/

  [style *= "width: 322px;"][style *= "position: relative;"]:not(:hover) > [style *= "padding: 10px 8px 8px; margin-top: -6px;"],
  [style *= "width: 322px;"][style *= "position: relative;"]:not(:hover) [style *= "border-width: 0px 0px 1px; border-color: rgb(43, 44, 51); border-style: solid;"],
  [style *= "width: 322px;"][style *= "position: relative;"]:not(:hover) [style *= "justify-content: space-between; padding: 10px; height: 44px"]
  {
    display: none !important;
  }

  [style *= "width: 322px;"][style *= "position: relative;"]:not(:hover) [data-text-as-pseudo-element="FAVOURITES"]:before {
    content: "  FAV";
  }

  [style *= "width: 322px;"][style *= "position: relative;"]:not(:hover) [data-text-as-pseudo-element="CHATS"] {
    text-overflow: inherit !important;
  }

  [style *= "width: 322px;"][style *= "position: relative;"]:not(:hover) [aria-roledescription="Conversation item"],
  [style *= "width: 322px;"][style *= "position: relative;"]:not(:hover) [aria-roledescription="Conversation item"] > div,
  [style *= "width: 322px;"][style *= "position: relative;"]:not(:hover) [aria-roledescription="Conversation item"] > div > div,
  [style *= "width: 322px;"][style *= "position: relative;"]:not(:hover) [aria-roledescription="Conversation item"] > div > div > div {
    height: 50px !important;
  }

  /**/

  [style *= "width: 322px;"][style *= "position: relative;"]:hover {
    width: 322px !important;
    box-shadow: 3px 0 5px 0 rgba(0, 0, 0, .2);
  }

  [style *= "width: 322px;"][style *= "position: relative;"] [style *= "margin-left: 4px; margin-top: 4px;"] {
    overflow: visible !important;
  }

  [style *= "width: 322px;"][style *= "position: relative;"] [style *= "background-color: rgb(0, 120, 212);"] {
    background-color: #f00 !important;
    position: absolute !important;
    top: 3px;
    left: -33px;
  }

  [style *= "margin: 5px 12px;"] {
    margin: 5px -9px !important;
  }
}

button[title="React to this message"] {
  opacity: .05;
}

button[title="React to this message"]:hover {
  opacity: 1;
}

body:before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 30px;
  z-index: 3;
  pointer-events: none;
  -webkit-app-region: drag;
}

Ignore the last instruction for now, we’re going to talk about it a bit later.

Everything inside @media {} styles the sidebar, except for the last instruction, which is there to remove some of the margins from the message text area. The two instructions outside @media are there to reduce the opacity of the “React to this message” button from all the message bubbles.

To keep track of these changes too, I create a new file called style.css and added it to my updates.json file.

{
    "js/app.js": {
        /* ... */
    },
    "css/skype.css": {
        "appendFile": "style.css"
    },
}

Removing the Title Bar

By default, Skype has a title bar that is added by your operating system, but Electron allows us to remove it and replace it with our own closing buttons. I didn’t like how that light grey of macOS looked with my dark interface, so I decided to remove it and skip the buttons altogether.

To do this, we need to edit app/MainWindow.js specifically config around the line:

'minHeight': exports.minimumWindowSize.height,

After it, I added a property called frame with the value of false.

This change will not work if you simply refresh the app. You need to close it and restart it to take effect.

To keep track of this change, I added the following to my updates.json file:

{
    "js/app.js": {
        /* ... */
    },
    "MainWindow.js": {
        "replace": {
             "/'minHeight': exports.minimumWindowSize.height,/": "'minHeight': exports.minimumWindowSize.height, 'frame': false,"
        },
    },
    "css/skype.css": {
        /* ... */
    },
}

That will append the frame: false to the minHeight line.

Adding Back the Dragging Ability

If you remove the title bar, on Windows and Linux you won’t be able to drag the window by anything else. On macOS, you can drag the window where the resizing icon appears. I, being a fan of minimalism, am content with that, but you might want to be able to drag the window by something else. Therefore, we need to inject something to make draggable.

Luckily, we can do all that from CSS. Simply add this to the css/skype.css file:

body:before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 30px;
  z-index: 3;
  pointer-events: none;
  -webkit-app-region: drag;
}

This creates a 30 px height bar at the top of the application that we can now drag the window by. The last property makes this draggable. If you want to make something else draggable, simply add that property. The pointer-events property makes this not consume any cursor events. Meaning that you are able to click through the invisible block even if it sits above everything else. If you want to see how this works, add a background: red to it and refresh the app.

We could inject HTML into one of the HTML files, but doing it in CSS saves us from having to inject one more file.

However, if you want to place any buttons, i.e. if you don’t like the shortcut, you need to inject the app/Index.html file to add the markup.

<div id="btn-minimize">_</div>

The code for minimizing is:

document.getElementById('btn-minimize').onclick = function () {
  electronApi.windowMinimize();
};

And you can add it at the end of js/base64.js file. Don’t add it to js/app.js because it breaks functionality.

You can style it from css/skype.css.

Use electronApi.windowToggleMaximize() for maximization and electronApi.windowClose() for closing.

Adding to the JavaScript Functionality

I’m content with how it looks now, but I still see an issue. If I click on link inside a conversation window, it opens, but the message box loses focus, and when I return and type something, it doesn’t type, because now the focus is on the link, and when I press Enter again to send the “ghost message,” it actually opens the link again. This is very annoying, and I want to focus on the message box when I come back, so I’m going to inject this in base64.js:

window.onload = function () {
  window.onfocus = function () {
    var textarea = document.querySelector("textarea");
    textarea && textarea.focus();
  };
};

This will focus the message box whenever we return to the Skype window and are in a conversation.

To keep track of that change, I’m going to create a new file called scripts.js and add it to the updates.json file.

{
    "js/app.js": {
        /* ... */
    },
    "MainWindow.js": {
        /* ... */
    },
    "js/base64.js": {
        "appendFile": "scripts.js"
    },
    "css/skype.css": {
        /* ... */
    },
}

Creating a Patch

We now have our updates.json:

{
    "js/app.js": {
        "replace": {
            "/black:\"#000000\"/i": "black:\"#2B2C33\"",
            "/#1E2224/gi": "rgba(0, 0, 0, .2)",
            "/#2B333B/gi": "rgba(0, 0, 0, .1)",
            "/#1F1F1F/gi": "#2B2C33",
            "/cardColor:i\\(6\\).color.gray400/gi": "cardColor:\"rgba(0, 0, 0, .15)\"",
            "/messageComposerBackgroundColor:i\\(6\\).color.gray400/gi": "messageComposerBackgroundColor:\"rgba(0,0,0,0)\"",
            "/,tiny:760,small:896/": ",tiny:0,small:0",
            "/,headerSizeSmall:24/g": ",headerSizeSmall:18"
        }
    },
    "MainWindow.js": {
        "replace": {
            "/'minHeight': exports.minimumWindowSize.height,/": "'minHeight': exports.minimumWindowSize.height, 'frame': false,"
        }
    },
    "js/base64.js": {
        "appendFile": "scripts.js"
    },
    "css/skype.css": {
        "appendFile": "style.css"
    }
}

And the related files, but we need a way to apply those changes automatically after we update Skype. So let’s create a patch that’s going to do this for us.

Create a folder somewhere where you will not lose it, like an iCloud, Dropbox, or Google Drive synced folder. I’ll call mine skype-patch. Then cd into it.

I’ll initialize npm inside this folder, just so I’ll be able to pull in some libraries to make our life easier.

npm init -y

This will generate a package.json configuration file for NPM.

Next, I’m going to pull in a library to read the updates.json file and the asar lib, just to have it locally, in case you don’t have it globally.

npm install -S load-json-file asar

The -S option will save it to the package.json file.

Next, I’m going to move style.css and scripts.js to a stubs dir.

And finally, I’m going to create a script to parse the updates.json file and apply the modifications.

const fs = require('fs');
const exec = require('child_process').exec;
const loadJson = require('load-json-file');
const updatesFile = 'updates.json';

/**
 * Validation.
 */

if (process.argv.length < 3) {
    console.log('You need to specify the installation location of Skype. It needs to be the folder where app.asar is located.');
    console.log('E.g.:   node patch.js /Applications/Skype.app/Contents/Resources');
    process.exit(0);
}

const skypeDir = process.argv[2];

if (! fs.existsSync(skypeDir)) {
    console.log('This directory doesn\'t exist.');
    process.exit(0);
}

if (! fs.existsSync(path('app.asar'))) {
    console.log('This directory doesn\'t contain "app.asar".');
    process.exit(0);
}

// Create a backup of the app.asar file if one doesn't already exist.
if (! fs.existsSync(path('app.asar.bak'))) {
    console.log('Creating a backup of app.asar because none exists.');
    fs.createReadStream(path('app.asar')).pipe(fs.createWriteStream(path('app.asar.bak')));
}

// Extract the asar file.
exec(`asar extract ${path('app.asar')} ${path('app')}`, () => {
    // Parse the updates file.
    loadJson(updatesFile).then(updates => {
        for (let file in updates) {
            let filePath = path(`app/${file}`);
            let update = updates[file];
            let contents = fs.readFileSync(filePath, 'utf8');

            console.log(`Processing ${file}...`);

            if (update.replace) {
                for (let regex in update.replace) {
                    let flags, substitute = update.replace[regex];

                    // Build a valid RegExp string.
                    regex = regex.substr(1);
                    regex = regex.split('/');
                    flags = regex.pop();
                    regex = regex.join('');
                    regex = regex.replace(/^#/, '\\#');

                    contents = contents.replace(new RegExp(regex, flags), substitute);
                }
            }

            if (update.appendFile) {
                let newContents = fs.readFileSync(`stubs/${update.appendFile}`, 'utf8');

                contents += newContents;
            }

            fs.writeFileSync(filePath, contents);
        }

        console.log('Removing the .asar archive because there\'s no need for it.');
        fs.unlinkSync(path('app.asar'));
    });
});

function path(to) {
    return skypeDir.replace(/\/+$/, '') + '/' + to.replace(/^\/+/, '');
}

To run it, we simply do:

node patch.js /Path/to/the/Skype/installation

If it was successful, the output will be:

Creating a backup of app.asar because none exists.
Processing js/app.js...
Processing MainWindow.js...
Processing js/base64.js...
Processing css/skype.css...
Removing the .asar archive because there's no need for it.

This will remove app.asar because you can’t use app.asar and app/ at the same time. If you need to run it again, you simply rename app.asar.bak into app.asar and run it again.

Conclusion

Hopefully, now you understand how Electron apps work and how powerful it is that you can update them as you please. If you wish to download the patch, it can be found here.