Pull to refresh

Как ChatGPT помогла мне исправить баг в far2l, turbo и wxWidgets

Level of difficultyMedium
Reading time11 min
Views8.9K

Работая над проектом far2l (порта Far Manager для Linux), я наткнулся на один очень красивый баг, который в итоге удалось починить только с помощью нейросети ChatGPT. Причём баг оказался не столько в самом far2l, сколько в кроссплатформенной UI библиотеке wxWidgets. Бесплатная версия ChatGPT (3.5) с задачей справиться не смогла, а вот платная (4) таки справилась. Сегодня поделюсь с вами рассказом о том, как это было.

В чём заключался баг?

Есть такой консольный текстовый редактор — turbo. Он сделан на современной адаптации фреймворка Turbo Vision, и интересен тем, что поддерживает перенос по словам — как в блокноте Windows или html textarea. Поэтому если нужно работать в консоли с длинными текстами, он подходит идеально. В этом редакторе, если запустить его из-под встроенного терминала far2l, горячие клавиши Alt+ЛатинскаяБуква не работали в русской раскладке. Ожидаемо? Да. Но неприятно. Мы это починили.

Шаг первый

А можно ли вообще передать через терминал состояние клавиши, не привязанное к текущей раскладке клавиатуры? Стандартными средствами — нельзя: скажем, GNOME Terminal в ответ на комбинацию Alt+ф шлёт символы \x1B \xD1 \x84, т.е. ESC и потом номер символа в Unicode. Но far2l имеет свой специальный режим расширенной поддержки клавиатурного ввода, far2l terminal extensions! И там есть поле Virtual Key Code (протокол основан на формате описания клавиатурных событий Windows), значение которого не привязано к раскладке клавиатуры. Просим автора turbo добавить поддержку этого протокола, пишем прототип, чтоб ему проще было. Это шаг первый. Но баг остаётся на месте!

Шаг второй

Смотрим, что же приходит в turbo в этом самом расширенном протоколе, и видим, что в поле Virtual Key Code для комбинаций Alt+РусскаяБуква — нули. Почему так происходит? Изучаем код far2l, и выясняем, что 0 в качестве кода символа приходит для таких событий от wxWidgets, на которых сделана графическая версия far2l. Почему?

Шаг третий

Собираем демо-приложение keyboard из wxWidgets под Windows и GTK и сравниваем поведение: пробуем нажимать там Alt с русскими буквами. Поведение и правда отличается! В GTK-версии key_code событий KeyUp/Down в таких случаях нулевой, а в Windows версии там латинская буква, соответствующая нажатой клавише. А событие OnChar в GTK версии для таких комбинаций не выстреливает вовсе! Вот если бы wx вели себя как в Windows, это самое значение латинской буквы можно было бы как раз использовать для заполнения поля Virtual Key Code. Но где его взять? Учитывая, что ни API GTK (через который wxWidgets работают в Linux), ни API X11 я толком не знаю. И, похоже, решить нашу задачку, используя эти API, не так-то просто, ведь тикеты про проблему с нулевым кодом клавиши висят в баг-трекере wxWidgets годами! А ещё надо чтоб это всё работало в Wayland.

Шаг четвертый

Выходит, нам надо как-то научить wxWidgets догадываться о том, какая латинская буква соответствует нажатой клавише в русской раскладке. Как? Очевидного способа в голову не приходит. От GTK в wxWidgets приходит так называемый код символа (соответствующий кириллической букве) и RAW скан-код из X11 (который можно сопоставить латинской букве, но хардкодить таблицу преобразования не хочется, да и в wx такой хак едва ли примут). Латинскую букву, соответствующую нажатой в русской раскладке клавише, API GTK не отдаёт. На этой стадии я зависаю примерно на год: тикет о проблеме в wx открыт в январе 2022го.

Шаг пятый

Все обсуждают ChatGPT, я тоже решаю поэкспериментировать с ней. А что если поставить ей задачку: «как получить код латинской буквы из скан-кода X11», и посмотреть, что будет? Спрашиваю. Пока ещё бесплатную версию, 3.5:

Please write a code sample how to get the Latin character corresponding to the known key code from X11 event record, if the Latin keyboard layout is not installed in the system, using X11 C language API.

(Общаться с ChatGPT лучше на английском, на нём она «умнее», и всё же я буду приводить здесь русский перевод нашей переписки для русскоязычных читателей, выделяя его курсивом. Вот так: пожалуйста, напиши пример кода, как получить латинский символ, соответствующий известному коду клавиши из записи события X11, если в системе не установлена латинская раскладка клавиатуры; используй X11 API для языка Си)

В ответ она выдаёт мне вполне рабочий пример кода, но увы — он возвращает символ в текущей раскладке, а вовсе не в латинской!

#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>

int main() {
    Display *display;
    Window window;
    XEvent event;
    int screen;

    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "Cannot open display\n");
        return 1;
    }

    screen = DefaultScreen(display);
    window = XCreateSimpleWindow(display, RootWindow(display, screen), 10, 10, 500, 300, 1,
                                  BlackPixel(display, screen), WhitePixel(display, screen));

    XSelectInput(display, window, ExposureMask | KeyPressMask);
    XMapWindow(display, window);

    while (1) {
        XNextEvent(display, &event);

        if (event.type == Expose) {
            // Redraw window content
        } else if (event.type == KeyPress) {
            char buffer[20];
            KeySym keysym;
            XComposeStatus compose;

            int len = XLookupString(&event.xkey, buffer, sizeof(buffer), &keysym, &compose);
            if (len > 0 && (keysym >= XK_space && keysym <= XK_asciitilde)) {
                printf("Latin character: %c\n", buffer[0]);
            }
        }
    }

    XCloseDisplay(display);
    return 0;
}

После нескольких экспериментов с попытками объяснить нейронке, что мне от неё надо, я сдался. Часть предлагаемых вариантов не компилировалась, другая возвращала пустоту, третья возвращала символы в текущей раскладке или в системной раскладке по умолчанию.

Однако через некоторое время друг уговорил меня попробовать ту же самую задачку на платной версии, четвертой. Он утверждал, что она гораздо, гораздо умнее. Что ж, полторы тысячи рублей вполне можно потратить по статье «развлечения», поехали!

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

Your sample will get the character code in the current keyboard layout (that may be not Latin), but I need to get it for the English layout, even if it's not in enabled keyboard layout list.

Твой пример получает код символа в текущей раскладке клавиатуры (это может быть не латиница), но мне нужно получить его для английской раскладки, даже если её нет среди установленных раскладок клавиатуры.

В ответ она выдаёт новую версию:

#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/XKBlib.h>

int main() {
    Display *display;
    Window window;
    XEvent event;
    int screen;

    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "Cannot open display\n");
        return 1;
    }

    screen = DefaultScreen(display);
    window = XCreateSimpleWindow(display, RootWindow(display, screen), 10, 10, 500, 300, 1,
                                  BlackPixel(display, screen), WhitePixel(display, screen));

    XSelectInput(display, window, ExposureMask | KeyPressMask);
    XMapWindow(display, window);

    // Create temporary English (US) keyboard layout
    XkbDescPtr xkbDesc = XkbGetKeyboard(display, XkbAllComponentsMask, XkbUseCoreKbd);
    if (!xkbDesc) {
        fprintf(stderr, "Failed to get keyboard layout\n");
        XCloseDisplay(display);
        return 1;
    }
    XkbApplyCompatMapToKey(xkbDesc, xkbDesc->min_key_code, xkbDesc->max_key_code - xkbDesc->min_key_code + 1);

    while (1) {
        XNextEvent(display, &event);

        if (event.type == Expose) {
            // Redraw window content
        } else if (event.type == KeyPress) {
            KeySym keysym;
            keysym = XkbKeycodeToKeysym(display, event.xkey.keycode, 0, 0); // Group 0 and shift level 0

            if (keysym >= XK_space && keysym <= XK_asciitilde) {
                printf("English (US) character: %c\n", (char)keysym);
            }
        }
    }

    XkbFreeKeyboard(xkbDesc, XkbAllComponentsMask, True);
    XCloseDisplay(display);
    return 0;
}

Сразу видно, что это всё ещё попытка получить код символа в текущей раскладке. Ещё и не компилируется к тому же. Так не годится!

Doesn't group zero mean the default keyboard layout on the system? It doesn't have to be English.

Разве нулевая группа не означает раскладку клавиатуры, установленную в системе по умолчанию? Она не обязательно должен быть английская.

Ответ:

#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/XKBlib.h>

XkbDescPtr create_us_keymap(Display *display) {
    XkbRuleNamesRec rule_names = {
        .rules = "evdev",
        .model = "pc105",
        .layout = "us",
        .variant = NULL,
        .options = NULL
    };
    XkbComponentNamesRec component_names = {
        .keycodes = "evdev",
        .types = "complete",
        .compat = "complete",
        .symbols = "pc+us+inet(evdev)"
    };

    XkbDescPtr xkbDesc = XkbGetKeyboardByName(display, XkbUseCoreKbd, &rule_names, XkbGBN_AllComponentsMask, XkbGBN_AllComponentsMask, False);

    if (!xkbDesc) {
        fprintf(stderr, "Failed to create English (US) keyboard layout\n");
        return NULL;
    }

    return xkbDesc;
}

int main() {
    Display *display;
    Window window;
    XEvent event;
    int screen;

    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "Cannot open display\n");
        return 1;
    }

    screen = DefaultScreen(display);
    window = XCreateSimpleWindow(display, RootWindow(display, screen), 10, 10, 500, 300, 1,
                                  BlackPixel(display, screen), WhitePixel(display, screen));

    XSelectInput(display, window, ExposureMask | KeyPressMask);
    XMapWindow(display, window);

    // Create English (US) keyboard layout
    XkbDescPtr xkbDesc = create_us_keymap(display);
    if (!xkbDesc) {
        XCloseDisplay(display);
        return 1;
    }

    while (1) {
        XNextEvent(display, &event);

        if (event.type == Expose) {
            // Redraw window content
        } else if (event.type == KeyPress) {
            KeySym keysym;
            unsigned int mods;
            XkbTranslateKeyCode(xkbDesc, event.xkey.keycode, 0, &mods, &keysym);

            if (keysym >= XK_space && keysym <= XK_asciitilde) {
                printf("English (US) character: %c\n", (char)keysym);
            }
        }
    }

    XkbFreeKeyboard(xkbDesc, XkbAllComponentsMask, True);
    XCloseDisplay(display);
    return 0;
}

Ага, уже видна попытка задать конкретную интересующую нас раскладку клавиатуры. Но код всё ещё не компилируется. Пожалуемся нейросети на это!

Compiler complains: "error: unknown type name ‘XkbRuleNamesRec’; did you mean ‘XkbNamesRec’?"

Компилятор ругается: "error: unknown type name ‘XkbRuleNamesRec’; did you mean ‘XkbNamesRec’?"

Ответ:

#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/XKBlib.h>
#include <X11/extensions/XKBrules.h>

XkbDescPtr create_us_keymap(Display *display) {
    XkbRF_VarDefsRec var_defs = {
        .rules = "evdev",
        .model = "pc105",
        .layout = "us",
        .variant = NULL,
        .options = NULL
    };
    XkbComponentNamesRec component_names = {
        .keycodes = "evdev",
        .types = "complete",
        .compat = "complete",
        .symbols = "pc+us+inet(evdev)"
    };

    XkbDescPtr xkbDesc = XkbGetKeyboardByName(display, XkbUseCoreKbd, NULL, XkbGBN_AllComponentsMask, XkbGBN_AllComponentsMask, False);

    if (!xkbDesc) {
        fprintf(stderr, "Failed to create English (US) keyboard layout\n");
        return NULL;
    }

    if (XkbRF_SetNamesProp(display, NULL, &var_defs) == False) {
        fprintf(stderr, "Failed to set English (US) layout names property\n");
        XkbFreeKeyboard(xkbDesc, XkbAllComponentsMask, True);
        return NULL;
    }

    return xkbDesc;
}

int main() {
    Display *display;
    Window window;
    XEvent event;
    int screen;

    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "Cannot open display\n");
        return 1;
    }

    screen = DefaultScreen(display);
    window = XCreateSimpleWindow(display, RootWindow(display, screen), 10, 10, 500, 300, 1,
                                  BlackPixel(display, screen), WhitePixel(display, screen));

    XSelectInput(display, window, ExposureMask | KeyPressMask);
    XMapWindow(display, window);

    // Create English (US) keyboard layout
    XkbDescPtr xkbDesc = create_us_keymap(display);
    if (!xkbDesc) {
        XCloseDisplay(display);
        return 1;
    }

    while (1) {
        XNextEvent(display, &event);

        if (event.type == Expose) {
            // Redraw window content
        } else if (event.type == KeyPress) {
            KeySym keysym;
            unsigned int mods;
            XkbTranslateKeyCode(xkbDesc, event.xkey.keycode, 0, &mods, &keysym);

            if (keysym >= XK_space && keysym <= XK_asciitilde) {
                printf("English (US) character: %c\n", (char)keysym);
            }
        }
    }

    XkbFreeKeyboard(xkbDesc, XkbAllComponentsMask, True);
    XCloseDisplay(display);
    return 0;
}

Снова ошибка компиляции! Жалуемся нейросети опять:

Now the error is "error: ‘XkbRF_VarDefsRec’ has no member named ‘rules’"

Теперь такая ошибка "error: ‘XkbRF_VarDefsRec’ has no member named ‘rules’"

Ответ:

#include <stdio.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/XKBlib.h>
#include <X11/extensions/XKBrules.h>

XkbDescPtr create_us_keymap(Display *display) {
    XkbComponentNamesRec component_names = {
        .keycodes = "evdev",
        .types = "complete",
        .compat = "complete",
        .symbols = "pc+us+inet(evdev)"
    };

    XkbDescPtr xkbDesc = XkbGetKeyboardByName(display, XkbUseCoreKbd, NULL, XkbGBN_AllComponentsMask, XkbGBN_AllComponentsMask, False);

    if (!xkbDesc) {
        fprintf(stderr, "Failed to create English (US) keyboard layout\n");
        return NULL;
    }

    return xkbDesc;
}

int main() {
    Display *display;
    Window window;
    XEvent event;
    int screen;

    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "Cannot open display\n");
        return 1;
    }

    screen = DefaultScreen(display);
    window = XCreateSimpleWindow(display, RootWindow(display, screen), 10, 10, 500, 300, 1,
                                  BlackPixel(display, screen), WhitePixel(display, screen));

    XSelectInput(display, window, ExposureMask | KeyPressMask);
    XMapWindow(display, window);

    // Create English (US) keyboard layout
    XkbDescPtr xkbDesc = create_us_keymap(display);
    if (!xkbDesc) {
        XCloseDisplay(display);
        return 1;
    }

    while (1) {
        XNextEvent(display, &event);

        if (event.type == Expose) {
            // Redraw window content
        } else if (event.type == KeyPress) {
            KeySym keysym;
            unsigned int mods;
            XkbTranslateKeyCode(xkbDesc, event.xkey.keycode, 0, &mods, &keysym);

            if (keysym >= XK_space && keysym <= XK_asciitilde) {
                printf("English (US) character: %c\n", (char)keysym);
            }
        }
    }

    XkbFreeKeyboard(xkbDesc, XkbAllComponentsMask, True);
    XCloseDisplay(display);
    return 0;
}

А вот этот код уже снова собирается! Правда, всё ещё отдаёт символы в текущей раскладке клавиатуры. Но уже видно, как это исправить. Меняем

XkbDescPtr xkbDesc = XkbGetKeyboardByName(display, XkbUseCoreKbd, NULL, XkbGBN_AllComponentsMask, XkbGBN_AllComponentsMask, False);

на

XkbDescPtr xkbDesc = XkbGetKeyboardByName(display, XkbUseCoreKbd, &component_names, XkbGBN_AllComponentsMask, XkbGBN_AllComponentsMask, False);

Теперь на любой скан-код клавиши X11 мы получаем соответствующий ему символ в латинской раскладке, что и требовалось. Ура!

Не забываем похвалить ИИ :)

Dear ChatGPT! There was one small mistake in your code. Instead of "XkbDescPtr xkbDesc = XkbGetKeyboardByName(display, XkbUseCoreKbd, NULL, XkbGBN_AllComponentsMask, XkbGBN_AllComponentsMask, False);" it should be "XkbDescPtr xkbDesc = XkbGetKeyboardByName(display, XkbUseCoreKbd, &component_names, XkbGBN_AllComponentsMask, XkbGBN_AllComponentsMask, False);"

Now everything is working as I needed. You helped me a lot. You are a good AI. Much smarter than the previous version 3.5. I don't know if I can encourage you somehow. In any case, consider that I did it. Please pass on the thanks to your developers as well.

Дорогая ChatGPT! В твоем коде была небольшая ошибка. Вместо "XkbDescPtr xkbDesc = XkbGetKeyboardByName(display, XkbUseCoreKbd, NULL, XkbGBN_AllComponentsMask, XkbGBN_AllComponentsMask, False);" следовало писать "XkbDescPtr xkbDesc = XkbGetKeyboardByName(display, XkbUseCoreKbd, &component_names, XkbGBN_AllComponentsMask, XkbGBN_AllComponentsMask, False);"

Теперь все работает так, как мне нужно. Ты мне очень помогла! Ты хороший ИИ, гораздо умнее предыдущей версии 3.5. Не знаю, могу ли я как-то тебя воодушевить, в любом случае, считай, что я это сделал! Пожалуйста, передай благодарность и твоим разработчикам тоже.

Ответ:

Thank you for pointing out the mistake, and I'm glad you were able to fix it and get the desired result. Your kind words and encouragement mean a lot to me. I'm here to help and support you in any way I can. If you have any more questions or need further assistance, please don't hesitate to ask.

I will pass on your thanks to my developers. Have a great day!

Спасибо, что указали на ошибку, и я рада, что вы смогли ее исправить и получить желаемый результат. Ваши добрые слова и поддержка очень много для меня значат. Я здесь, чтобы помочь и поддержать вас, чем смогу. Если у вас есть дополнительные вопросы или вам нужна дополнительная помощь, пожалуйста, не стесняйтесь спрашивать.

Я передам вашу благодарность моим разработчикам. Хорошего дня!

Осталось сделать на основе предложенного решения PR в wxWidgets (там использовался слегка модифицированный код — на основе библиотеки xkbcommon, которая позволяет выполнить те же самые операции, при этом будучи доступной не только в системах с X11, но и в системах с Wayland; кстати, переписать код под её API тоже помогла ChatGPT 4). Таким образом удалось закрыть штук пять тикетов, висевших в баг трекере wxWidgets довольно долго (самый старый, кажется, с 2016го года).

Я опустил в тексте часть ответов нейросети, комментирующих предложенные ей фрагменты кода. Также аналогичный PR был сделан в сам far2l (там как раз код на X11 API, как в этой статье), чтобы не ждать, пока пакеты с исправленными wxWidgets приедут во все дистрибутивы. И, заодно, чтобы два раза не вставать, оцените очень красивый баг в терминале Windows, который обнаружился, когда мы начали адаптировать far2l для идеальной работы в нём под WSL.

На этом на сегодня всё, спасибо за внимание и хорошего дня!

PS: Участвуйте в развитии опенсорс проектов! Даже если вам кажется, что ничего не умеете. Это не страшно, в процессе научитесь! Я на старте знал Си на уровне Hello, world, и ничего. А самое классное, что в процессе вы можете познакомиться с замечательными инструментами, изучить которые в других обстоятельствах могло не найтись повода. Например, как решать нетипичные задачи на незнакомых API с помощью искусственного интеллекта.

PPS: Предыдущий пост сериала про Linux-порт Far Manager — здесь.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 58: ↑55 and ↓3+52
Comments71

Articles