Romaxx / mongodbcompass_darktheme

11 stars 1 forks source link

Automate application #1

Open ricardo-reis-1970 opened 3 years ago

ricardo-reis-1970 commented 3 years ago

Dear authors,

Intro

This is neither an issue nor a pull request. It's just that I found no better place to submit it. I believe this could be helpful to many people.

Context

I don't use this theme as a coolness statement. I am partially sighted and the whole white background trend instituted by fancy architects and designers really hurts my eyes. However, if you're like me, or just work at night, or like it dark, it's all good reasons to use this.

Motivation

In Step 2, we're told from the start that in order to use this theme, we must repeat the outlined steps every time we run Compass. I absolutely understand the reason for this, as the option would be to inject code in an Electron app, but I don't like the trouble.

Plus, there is a series of resources to learn this, such as:

Solution

Since what I've been doing the most is Python, this was the weapon of choice. This relies on a Windows automation library called pywinautoPypi, Github and ReadTheDocs — that acts as Selenium but for Windows Desktop UIs.

Note: Compass seems to be a Chrome-like application, so I am not sure Selenium would not be an option, but online posts seem to suggest otherwise.

For the development of this, I had to look into the UI elements of Compass, so I relied on inspect.exe, that I found in C:\Program Files (x86)\Windows Kits\10\bin\10.0.19041.0\x64\inspect.exe, on my Windows 10 laptop.

Here's the code:

from pywinauto.application import Application
from pywinauto import ElementNotFoundError

escapes = "{}()"
filt = lambda s: ''.join(map(lambda c: f'{{{c}}}' if c in escapes else c, s))
program = [
    "console.clear()",
    "let jq = document.createElement('script')",
    "jq.src = './../../../darkreader.min.js'",
    "document.getElementsByTagName('head')[0].appendChild(jq)",
    """DarkReader.enable({
    brightness: 100,
    contrast: 90,
    sepia: 10
});"""
]

try:
    mng = Application(backend="uia").connect(title="MongoDB Compass - Connect")
except ElementNotFoundError:
    mng = Application(backend="uia").start("MongoDBCompass.exe")
print('Waiting on top winodow...')
timeout = 100
while timeout and len(mng.window().descendants()) < 200:
    timeout -=1
print('Done!')
view = list(filter(lambda e: e.texts()==['View'], mng.window().descendants()))[0]
consoles = list(filter(lambda e: e.texts()==['Console'], mng.window().descendants()))
if consoles == []:
    print('Opening console.')
    view.type_keys('%^I')
    while consoles == []:
        consoles = list(filter(lambda e: e.texts()==['Console'], mng.window().descendants()))
    print('Console open.')
else:
    print('Console already open.')

console = consoles[0]
console.click_input()
while not console.is_selected():
    pass
console.type_keys("var jq = document.createElement('script')")

code = list(filter(lambda e: e.texts()==['Code editor'], mng.window().descendants()))[0]
code.is_visible = lambda : True
code.type_keys("{END}+{HOME}")
for line in program:
    print(f'Typing:\n{line}\n as\n{filt(line)}~\n\n')
    code.type_keys(f'{filt(line)}~', pause=.05, with_spaces=True, with_tabs=True, with_newlines=True)
view.type_keys('%^I')

I called the file darkCompass.py, so I run it as python darkCompass.py.

Code overview

A number of assumptions are made, as I run on a controlled environment where I know what is available. The script assumes that Compass is installed, so it starts by connecting to it. Failing to do so means Compass is not running, so the script moves on to starting it, again assuming that it is in the path. After this, it's assumed that Compass is up and running.

Then, it counts the number of descendants of the main window. If this is less than 200, it cycles over a lame timeout mechanism (begging improvement). The issue here was that while loading there are much fewer descendants and at this stage the script would not do its job.

It then checks whether the console is already open and, if not, sends the keystroke combination to open the console. This is being sent to the object representing the meny View, but any such object able to receive keystrokes would do. Then it cycles waiting on the console to open.

Afterwards, it grabs the code editor window element and starts typing the lines — stored in program. Note that before it starts, it goes to the end and selects the whole line. This is done in case there is some text typed but not entered in the console, in which case it will be discarded (replaced by the first line in program).

Each line in program is sent to the console, but not without being filtered. In fact, some characters are supressed from the script, unless they are surrounded by curly braces, including parenthesis and curly braces themselves. In order to not hurt the leginility of the code stored in program, this filtering is done outside. This way, if you need/wish to edit parts, particularly the colour parameters in DarkReader.enable, you won't need to hack through a confusing encoding.

Characters in need of encoding are held in escapes. Spaces also needed to be encoded, until I found out the parameter with_spaces to the type_keys method, same for newlines.

Finally, the console is closed and your beautiful dark Compass is ready to be used.

Usage

As per the above description, we can use this script with or without Compass open, as it will open Compass, should it not be. Also, if the console is not open, it will be. However, note that regardless of the console's initial state, it will be closed at the end. I don't see this as very negative, considering that Chrome console open in Compass is a very rare event.

Final notes

This is my absolute first shot at automation. Skipping over enormous amounts of pride, I will be carrying forward with improvements, further tests and possibly adding features. A repository for this will be created and contributions are very welcome.

Upon inspection, I realised that most of the UI nodes have no label. Although not thought of for automation, as UIs never seem to be, I think that this is a bad practice and it should be avoided at all costs. The actual cost of avoiding this is almost zero, just adding ID labels. Without labels, all we can do is either render a full node tree — which might not be available if the target elements are not actually rendered on screen, and would always be expensive — or go step by step with tools like inspect, a tedious task.

If you check it out, you'll see that pywinauto seems to yield a lot of possibilities, but alas its documentation runs a bit shallow and the latest release is from Oct 27 2019. It still works, but all this adds to the trial-and-error approach.

If this serves the purpose of automating dark mode in Compass, then its purpose is fulfilled. If what you find here encourages you to automate other Windows tasks, I will be very happy.

Romaxx commented 3 years ago

Hello, nice work, I'm working on Linux so I prefer "multiplatform" solution, but you're solution is nice for windows users.

ricardo-reis-1970 commented 3 years ago

Thank you!

I actually haven't tested this in any Linux variant, because I'm not lucky enough to work in it and I'm still trying to choose the best for my personal laptop. Manjaro and PopOS came well recommended and I'm unable to choose right now.

Back to the topic, according to the PyWinAuto readme.md, is should already be usable in Linux. Whether Gnome or KDE, remains to be seen.

I don't like this Windows mess and I decided that I need to know some automation as part of my boasting the title of software engineer. So, if you have any automation suggestion that:

  1. is free;
  2. is portable;
  3. has a last commit more recent than the Reagan / Gorbachov administration; I am all ears.