Внимание, утилита обновлена! Скрипт превратился в модульный комбайн для shell-конвейеров. Подробности и описание новых флагов (-X, -x, -S, -s и др.) ищите в UPD в конце статьи.

Уже давно весьма огорчаюсь тем, что в моём любимом Gpick отвалилась пипетка с тех пор, как KDE со своей Плазмой прочно переехали на Wayland. Если раньше я ещё мирился с GTK-шностью Gpick (про KDE, плазму и вяленого — поручик, молчать!), то теперь совсем взгрустнулось…

Началось всё с того, что я напрочь забыл, какие у меня в «Местах» цвета какие. Я же человек правильный, нормальные человеческие названия для переменных ведь не путь джедая, верно? Посмотрев на своё

@mixin theme {
    $color-01: rgba($col-36, $opacity-07);
    $color-02: rgba($col-36, $opacity-11);
    $color-03: rgba($col-36, $opacity-13);
    $color-04: rgba($col-36, $opacity-12);
…

(и ещё такого на пару сотен строк только в одном файле), я прослезился, ударил стену головой и решил исправляться к лучшему. Заодно перейти на адекватные CSS-переменные, а не этот SCSS-хардкор. Но вот без пипетки это грустновато…

Итак, нужна, прости Господи, пипетка. Ставить и терпеть ещё и штатный kcolorpicker, а уж тем более, kcolorchooser (при живом-то, ну, почти, Gpick) в своём уютном Artix я не намерен. Ладно про настройки я молчу, в которых у KDE-шных поделий обычно по две штуки — «Показывать иконку справа вместо слева» и «Рассказывать анекдоты раз в час вместо раза в полчаса». Но функционал… Непосредственного нету, а посредственного на полгига.

hyprpicker, который, как написано в пояснялке, does not suck, не просто wlroots-compatible. Проблема в том, что это я сейчас «wlroots-incompatible». Что остаётся? Правильно. Велосипедить.

Стал пытаться думать мозгом. Ударился было в bash-скрипты, но довольно быстро работать там с DBus, асинхронностью порталов и прочими мониторами стало очень больно.

Сунулся было набросать простенькую утилитку на C++/Qt (последний оборот звучит, правда? 😁) — стало ещё больнее. Ну, вы знаете. Для того, чтобы в дверь врезать новый замок, нам жизненно необходимо вокруг неё построить десятимиллионный мегаполис с инфраструктурой, аэропортом, полицией и трамвайной сетью. Не говоря уже о заведениях с преферансом и барышнями…

Короче, плюнул и наваял скрипт на Питоне (хоть я его и терпеть не могу). Получилось, имхо, просто прекрасно. При запуске даёт тебе пипетку, ждёт, пока жмакнешь ей где-нибудь на экране, и выдаёт на выход сразу пачку CSS-значений на любой вкус в таком виде:

#7e80ea  
rgb(126 128 234)  
rgb(49.4% 50.2% 91.8%)
hsl(238 72% 70%)  

Тут тебе и RGB, хоть в HEX, хоть в числах байтов, хоть в процентах, и HSL. А ежели запускать ещё и с соответствующими ключиками (описание см. в UPD в конце статьи; да, я допиливаю), то заодно копирует и/или выводит только нужные значения в буфер обмена и/или в stdout. Можно повесить на хоткеи хоть голым, хоть через какую-нибудь shell-трубу. Красота! 😁

В общем, делюсь с городом и миром. Может, кому-то пригодится и сэкономит нервы и время:

#!/usr/bin/env python3
import sys
import colorsys
import subprocess
import argparse
from gi.repository import GLib, Gio

parser = argparse.ArgumentParser(
    description="Wayland XDG-Portal Color Picker"
)

group_output = parser.add_argument_group("Standard output")
parser.add_argument(
    "-X", "--hex",
    action="store_true", help="HEX to output",
)
parser.add_argument(
    "-S", "--hsl",
    action="store_true", help="HSL to output",
)
parser.add_argument(
    "-B", "--rgb-bytes",
    action="store_true", help="RGB (bytes) to output",
)
parser.add_argument(
    "-P", "--rgb-percentages",
    action="store_true", help="RGB (percentages) to output",
)

group_copy = parser.add_argument_group("Copy to clipboard")
parser.add_argument(
    "-x", "--copy-hex",
    action="store_true", help="HEX to clipboard",
)
parser.add_argument(
    "-s", "--copy-hsl",
    action="store_true", help="HSL to clipboard",
)
parser.add_argument(
    "-b", "--copy-rgb-bytes",
    action="store_true", help="RGB (bytes) to clipboard",
)
parser.add_argument(
    "-p", "--copy-rgb-percentages",
    action="store_true", help="RGB (percentages) to clipboard",
)

args = parser.parse_args()

args_output = [
    args.hex,
    args.hsl,
    args.rgb_bytes,
    args.rgb_percentages,
]
args_copy = [
    args.copy_hex,
    args.copy_hsl,
    args.copy_rgb_bytes,
    args.copy_rgb_percentages,
]

loop = GLib.MainLoop()
channels = None

bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)

def on_signal_received(
    connection,
    sender_name,
    object_path,
    interface_name,
    signal_name,
    parameters,
    user_data,
):
    global channels
    try:
        code, results = parameters.unpack()
        if code == 0 and 'color' in results:
            channels = results['color']
    except Exception as e:
        sys.stderr.write(f"Signal error: {e}\n")
    finally:
        loop.quit()

def on_method_reply(connection, res):
    try:
        reply = connection.call_finish(res)
        request_path = reply.unpack()[0]
        
        bus.signal_subscribe(
            "org.freedesktop.portal.Desktop",
            "org.freedesktop.portal.Request",
            "Response",
            request_path,
            None,
            Gio.DBusSignalFlags.NONE,
            on_signal_received,
            None,
        )
    except Exception as e:
        sys.stderr.write(f"Method error: {e}\n")
        loop.quit()

bus.call(
    "org.freedesktop.portal.Desktop",
    "/org/freedesktop/portal/desktop",
    "org.freedesktop.portal.Screenshot",
    "PickColor",
    GLib.Variant("(sa{sv})", ("", {})),
    None,
    Gio.DBusSignalFlags.NONE,
    -1,
    None,
    on_method_reply,
)

loop.run()

if channels and len(channels) >= 3:
    r_f, g_f, b_f = channels[0], channels[1], channels[2]

    r_b, g_b, b_b = int(r_f * 255), int(g_f * 255), int(b_f * 255)
    r_p, g_p, b_p = r_f * 100, g_f * 100, b_f * 100
    h_f, l_f, s_f = colorsys.rgb_to_hls(r_f, g_f, b_f)
    h, s, l = int(h_f * 360), int(s_f * 100), int(l_f * 100)

    color_map = {
        "hex": f"#{r_b:02x}{g_b:02x}{b_b:02x}",
        "rgb_bytes": f"rgb({r_b} {g_b} {b_b})",
        "rgb_percentages": f"rgb({r_p:.1f}% {g_p:.1f}% {b_p:.1f}%)",
        "hsl": f"hsl({h} {s}% {l}%)",
    }

    if not any(args_output):
        print("\n".join(color_map.values()))
    else:
        for key, value in color_map.items():
            if getattr(args, key):
                print(value)

    if any(args_copy):
        lines = []
        for key, value in color_map.items():
            if getattr(args, f"copy_{key}"):
                lines.append(value)

        output = "\n".join(lines)

        try:
            subprocess.run(
                ['wl-copy', output],
                check=True,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
        except Exception:
            try:
                p = subprocess.Popen(
                    ['xclip', '-selection', 'clipboard'],
                    stdin=subprocess.PIPE,
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL,
                )
                p.communicate(input=output.encode())
            except Exception:
                pass

    sys.exit(0)
else:
    sys.stderr.write("Cancelled by the user.\n")
    sys.exit(1)

А если лениво отсюда копировать, закоммитил в своём GitHub-овском репозитории других полезняшек: ds-colorpicker.

Вот. Пользуйтесь, и больше велосипедов богу СПО! 😁


UPD: Почесал ещё репу и прокачал скрипт до полноценного комбайна. Теперь поддерживаются 8 флагов для выборочного вывода и копирования в буфер обмена конкретных форматов.

По умолчанию (без флагов) утилита теперь выводит все значения на выход. А вот с флагами теперь так. Они разделены на две группы по четыре. Никто никого не исключает, можно комбинировать как угодно.

Флаги вывода (stdout):

  • -X — HEX-значение

  • -S — HSL-значение

  • -B — RGB-значение (в числах байтов)

  • -P — RGB-значение (в процентах)

Флаги копирования в буфер обмена:

  • -x — HEX-значение

  • -s — HSL-значение

  • -b — RGB-значение (в числах байтов)

  • -p — RGB-значение (в процентах)

Как видите, флаги обеих групп отличаются друг от друга только регистром. Большие — для вывода в консольку/конвейер, маленькие — для копирования в буфер обмена.

То есть, теперь этот скрипт очень удобно вызывать в конвейерах, по хоткеям. Например, так:

echo "color: $(ds-colorpicker -B);" >> styles.css

Код в статье и репозитории обновлён. Все подробности по ключам — в ds-colorpicker --help.