# Auto adapting signal to day-night scheme So I wanted to automatically change the day/night theme of my brand new Signal desktop app. The way that it is manually done is through the `File -> Preferences...` menu. Now that's not convinient for programatic person like me. Let's investigate! ## `.config` folder My first stab is looking into the mostly standard `~/.config` folder in Linux for Signal if one actually exists. It turns out it does. Let's see. It even has a `~/.config/Signal/config.json` file. Unfortunately, that file doesn't contain the settings for the theme, just window positions. `grep -r`ing in the folder for any of `android` or `theme` just returns the logfile that Signal is writing to. ``` ... ./logs/log.log.1:{..."theme-setting changed to android-dark",...} ... ``` No luck. Another stab is at `/opt/Signal` directory. Again - no luck. And the app is a binary ELF executable ``` $ file signal-desktop signal-desktop: ELF 64-bit LSB executable,... ``` ## `strace`-ing My next attempt is to attach to attach to the Signal process via `strace`. Find the process by `ps aux|grep signal`, attach by `strace -p ` and try to change the . Boy, this does a lot of stuff! Let's filter by writing: `strace -e write -p 12345` Still, too much but I saw something hapenning amid the writing to `40` and `71` file descriptors (fd). ```bash #... write(40, "!", 1) = 1 write(71, "\0", 1) = 1 write(40, "!", 1) = 1 write(1, "{\"name\":\"log\",\"hostname\":\"pi2-ho"..., 147) = 147 write(12, "\1\0\0\0\0\0\0\0", 8) = 8 write(71, "\0", 1) = 1 write(40, "!", 1) = 1 #... ``` All right, the `1` fd is the log that is being written that I already saw. How about that 12 fd? Let's explore within the `proc` fs: ```bash $ cd /proc/1234/fd $ ls -la # ... lrwx------ 1 pi2 pi2 64 Mar 29 23:23 12 -> anon_inode:[eventfd] # ... ``` Oh boy, some wierd anonymous inode. So we are definately not dealing with a good old filesystem file but rather some on the fly created fd. Let's explore a bit more... ## Signal is an Electron app Taking a look at the [github repo of the project](https://github.com/signalapp/Signal-Desktop). Searching for `dark` and `theme` reveals css, sass files. Okay Looking at the menus there is `View -> Toggle Developer Tools` and BAM - a good old Chrome developer tools console pops up! Inspecting the elements, it's pretty clear what is happening: ```html ``` Brilliant, the change of theme just changes the class on the `body` element. Let's see if we have jquery or I have to go to the dark ages of javascript's `findElementByTagName`: ```javascript > $ < function ( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' ... ``` Good, we have. Now I "just" have to find a way to connect to chrome console via a good old terminal in some way... ## Chrome remote debugging Searching the Internets, I find a [post](https://blog.chromium.org/2011/05/remote-debugging-with-chrome-developer.html) in Chromium blog from way back in 2011 that I can run Chrome with `--remote-debugging-port` flag which enables some sort of a remote console. Would the electron app do the same? ```bash signal-desktop --remote-debugging-port=9222 ``` Indeed it does. This console however is not just a simple tcp connection to the javascript console. Running `echo "alert('echo'); | netcat localhost 9222` does nothing. More reading to go... Turns out, Chrome DevTools has its [own protocol](https://chromedevtools.github.io/devtools-protocol/) that among other things exposes websockets. Let's see what clients exist for that [Quite a lot](https://github.com/ChromeDevTools/awesome-chrome-devtools). Any pythonic ones? [pychrome](https://github.com/fate0/pychrome) seems simple enough. Let's be modern, do python3: ```bash pip3 install pychrome ``` And I will use my favourite repl, `ipython` and follow the Getting started tutorial: ```python import pychrome browser = pychrome.Browser(url="http://127.0.0.1:9222") ``` Awesome, I have a connection. The tutorial goes into opening a tab. I don't want that, I assume I should alread have a tab. Exploring `browser` object (via my favourite [ipython repl](http://ipython.org/)): ```python In [3]: dir(browser) Out[3]: [... _private_stuff 'activate_tab', 'close_tab', 'dev_url', 'list_tab', 'new_tab', 'version'] In [4]: browser.list_tab() Out[4]: [] In [5]: tab = browser.list_tab()[0] In [6]: dir(tab) Out[6]: [... _private_stuff 'call_method', 'debug', 'del_all_listeners', 'event_handlers', 'event_queue', 'get_listener', 'id', 'method_results', 'set_listener', 'start', 'status', 'status_initial', 'status_started', 'status_stopped', 'stop', 'type', 'wait'] ``` The README goes into navigating pages and the examples folder is also not what I am looking for. But they do tell me `call_method` is probably what I want, just need to find the proper string and parameters in the protocl. I want to execute a javascript call. Back to the [Chrome DevTools Protocol Viewer](https://chromedevtools.github.io/devtools-protocol/). I look through a few of the Domains listed and finally arrive at [Runtime.evaluate](https://chromedevtools.github.io/devtools-protocol/tot/Runtime#method-evaluate) method. ```python In [7]: tab.start() In [8]: tab.call_method('Runtime.evaluate', expression='alert("hi");') # I GET AN ALERT IN THE BROWSER! Dismiss it. Out[8]: {'result': {'type': 'undefined'}} ``` Finally! I have access to my javascript console in the browser! Let's write the jquery change of theme: ```python tab.call_method('Runtime.evaluate', expression='$("body").removeClass("android").addClass("android-dark");') ``` That worked! ## Wrapping it up Let's write a propper python script now: ```python #!/usr/bin/env python3 import sys import pychrome sys_argv = sys.argv if len(sys_argv) < 2: print("first argument needs to be day|night") exit(1) command = sys_argv[1] if command == "night": expression = js_dark_to_light = """ $("body").removeClass("android").addClass("android-dark"); """ elif command == "day": expression = js_light_to_dark = """ $("body").removeClass("android-dark").addClass("android"); """ else: print("first argument needs to be day|night") exit(1) browser = pychrome.Browser(url="http://127.0.0.1:9222") tab = browser.list_tab()[0] tab.start() tab.call_method('Runtime.evaluate', expression=expression) tab.stop() ``` Running `./change_signal.py day` changes to white theme and `./change_signal.py night` goes to dark theme. I also want to change the `signal-desktop` app to always start with the remote-debug flag enabled, so I do that: ```bash rm /usr/local/bin/signal-desktop cat > /usr/local/bin/signal-desktop << EOF #!/bin/bash /opt/Signal/signal-desktop --remote-debugging-port=9222 EOF chmod +x /usr/local/bin/signal-desktop ``` And that's it. Code can be found in my [sun](https://gitlab.com/pisquared/sun) repo which also deals with other day-night related dark-light themes including a script to detect whether the sun has set or not: ```bash $ ./calc_sun.py usage: calc --loc=location_name [--date-format] calc --lat=latitude --lon=longitude [--tz|--date-format] returns current_time sunrise sunset day|night $ ./calc_sun.py --lat=51.21 --lon=4.40 --tz=Europe/Brussels 23:57 07:23 20:10 night ``` So now I can chain that: ```bash state=`./current.sh | cut -f4 -d' '` ./change_signal.py $state ``` Voala! That was fun 🙂😊