the Worthless Writeup Library @ szy.lol

laptopctl

work started on ??

laptopctl is a tiny bespoke control panel for my bespoke laptop setup.


Screenshot of laptopctl

This is the window that pops up when I press Win+C on my keyboard, short for Control. The six buttons, each with its own keybind, control tiny matters related to portability.

My main computer is a laptop, a gaming laptop at that, and an old gaming laptop at that*, so power management is important. By pressing c on my keyboard, or the upper right button, the CPU energy bias is toggled between “performance” (0), “balanced” (7), and “power saving”. Honestly, I don’t feel it having much of an effect, but it’s there.

c’s more loved sibling is f, the lower right button controlling the CPU clock frequency range. Its label shows the current frequency, flanked by the minimum and maximum. Clicking the button or its hotkey selects between the maximum frequencies of 1GHz, 2GHz and 4GHz – you can actually feel the difference between them, both in performance and battery usage.

The other pair of buttons are ext1 and ext2, hotkeyed to 1 and 2. These control the mounting of two external HDDs I use. I don’t remember why I did it that way, but they are mounted via systemd .mount units, and this button runs either systemctl start or systemctl stop. Deluge (the torrent client, d) both doesn’t like being started with the files it’s seeding gone, nor does it enjoy them being slid from under itself, and it makes unmounting impossible because it makes the drive “busy”, so it gets a button too.

Last one is w, controlling the killswitch for Wi-Fi. It’s useful to force the computer to use cabled Ethernet, and not distract itself with the flaky wireless.

All in all, this makes for an “unplugging sequence” of dwffcc12q, and cf12wdq when I’m back home. Pretty convenient, especially since it was a series of commands before.

The implementation is very simple, it’s a Python script based on tkinter that draws the buttons and runs commands when they are pressed. It’s bound to the key combination in xfce’s Keyboard settings, under Application Shortcuts. Quite simple, and reasonably effective. If you care for hacked together Python, with heavy use of subprocesses and regex parsing, here is the listing. It uses OOP and inheritance (!!), so it starts out very ugly but the final 20 lines are somewhat pretty at least.

Listing
#!/bin/env python3
import tkinter as tk
from tkinter import ttk
import subprocess
import json
import re

root = tk.Tk()
root.title("laptopctl")
root.attributes("-topmost", True)
frm = ttk.Frame(root, padding=10)
frm.focus_set()
def kbind(evt, fn):
    def a(*args):
        fn()
    root.bind_class('.', evt, a)
kbind('q', root.destroy)
kbind('<Escape>', root.destroy)
frm.pack(expand=1)

class Button:
    def state(self):
        raise NotImplementedError('notimpl')
    def change(self):
        raise NotImplementedError('notimpl')
    def __init__(self, frm, c, r, bind):
        self.s = tk.StringVar()
        self.s.set('...')
        self.b = ttk.Button(frm, command=self.toggle, textvariable=self.s)
        self.b.grid(column=c, row=r)
        self.b['command'] = self.toggle
        self.frm = frm
        kbind(bind, self.toggle)
        self.frm.after(0, self.update)
    def update(self):
        try:
            self.s.set(self.state())
        except Exception as e:
            self.s.set('E:'+str(e))
        self.frm.after(5000, self.update)
    def toggle(self):
        try:
            self.change()
        except Exception as e:
            self.s.set('E:'+str(e))
            self.frm.after(1000, self.update)
        else:
            self.s.set('OK')
            self.frm.after(200, self.update)

class Rfkill(Button):
    last = '_'
    def state(self):
        p = subprocess.run(
            'rfkill -o SOFT -r -n list wlan'.split(' '),
            capture_output=True
        )
        s = p.stdout.decode().strip()
        self.last = s
        return 'wifi: '+s
    def change(self):
        tab = {'blocked': 'unblock', 'unblocked': 'block'}
        n = tab.get(self.last)
        if n is None:
            raise Exception('unk state: '+self.last)
        subprocess.run(['sudo', 'rfkill', n, 'wlan'])
        self.last = '_'

class SysdService(Button):
    on = -1
    name = 'ChangeMe'
    prettyname = None
    sudo = False
    user = False
    def state(self):
        cmd = ['systemctl', 'is-active', '--user' if self.user else None, self.name]
        p = subprocess.run(
            filter(lambda x: x is not None, cmd),
            capture_output=True
        )
        state = p.stdout.decode().strip()
        self.on = p.returncode == 0
        return (self.prettyname or self.name)+': '+state+('' if self.on else '!')
    def change(self):
        acts = {True: 'stop', False: 'start'}
        act = acts.get(self.on)
        if act is None:
            return
        cmd = ['pkexec' if self.sudo else None, 'systemctl', act, '--user' if self.user else None, self.name]
        subprocess.Popen(
            filter(lambda x: x is not None, cmd),
            stdin=None, stdout=None, stderr=None, close_fds=True
        )

class Cpu(Button):
    last = -1
    def state(self):
        p = subprocess.run('sudo cpupower info -b'.split(' '),
                capture_output=True)
        m = re.search(r'perf-bias: (\d+)', p.stdout.decode())
        if not m:
            self.last = -1
            return 'cpu: unk'
        self.last = int(m.group(1))
        desc = {0: 'perf', 7: 'bal', 15: 'pow'}
        return 'cpu: %d (%s)' % (self.last, desc.get(self.last, '?'))
    def change(self):
        if self.last == -1:
            raise Exception('unk state: '+self.last)
        tab = {0: 7, 7: 15, 15: 0}
        n = tab.get(self.last, 15)
        subprocess.run(['sudo', 'cpupower', 'set', '-b', str(n)])

class Cpuf(Button):
    last = -1
    freqs = {'4.00 G': 4, '2.00 G': 2, '1.00 G': 1, '1000 M': 1}
    def state(self):
        p = subprocess.run('cpupower frequency-info'.split(' '),
                capture_output=True)
        s = p.stdout.decode()
        r = re.search(r'within ([0-9.]+ .)Hz and ([0-9.]+ .)Hz', s)
        mn, mx = '?', '?'
        if r:
            mn, mx = r.group(1), r.group(2)
        self.last = self.freqs.get(mx, -1)
        c = re.search(r'current CPU frequency: ([0-9.]+ .)Hz', s)
        cur = '?'
        if c:
            cur = c.group(1)
        return ':'.join([x.replace(' ', '') for x in (mn, cur, mx)])
    def change(self):
        tab = {4: 2, 2:1, 1:4}
        n = tab.get(self.last, 2)
        subprocess.run(['sudo', 'cpupower', 'frequency-set', '-u', f'{n}G'])

class Ext1(SysdService):
    name = 'mnt-ext.mount'
    prettyname = 'ext1'

class Ext2(SysdService):
    name = 'mnt-ext2.mount'
    prettyname = 'ext2'

class Deluge(SysdService):
    name = 'deluge'
    user = True

Rfkill(frm, 0, 0, 'w') # for wifi
Deluge(frm, 1, 0, 'd')
Cpu(frm, 2, 0, 'c')
Cpuf(frm, 2, 1, 'f')
Ext1(frm, 0, 1, '1')
Ext2(frm, 1, 1, '2')
root.mainloop()

* None of the performance, none of the battery life – a compromise so great the UN might weep.