Автоматизации

Необязательный объект automation в файле .clinkplugin объявляет события, состояние, настройки, команды, готовые правила и слоты запуска. У каждого компонента есть стабильный ID, точная версия схемы и локализованные названия. Хост создаёт имена plugin/<plugin-id>/<kind>/<id>. Плагин не может занять системное пространство имён или пространство другого плагина.

Автоматизации ›

Как работают плагины

Панель заменяет клавиши, а действие один раз обрабатывает фрагмент текста. У плагина нет собственного экрана на клавиатуре. Clink вызывает его функции в определённые моменты, например когда открывается клавиатура или вы заканчиваете слово, и плагин может обновить пробел или своё сохранённое состояние. Его настройки рисуются в приложении теми же конструкторами, что и панели. Ниже приведён WPM Spacebar, полноценный плагин.

def initial():
    return {"on": False}

def settings(state):
    return section("keys.spacebar", [
        toggle("Show typing speed", state["on"], action="toggle"),
    ], title="WPM Spacebar")

def on_action(action, value, state):
    state["on"] = value
    if value:
        claim("spacebar.text")
    else:
        release("spacebar.text")
        space_text(None)
    return state

def on_word(word, state):
    if state["on"]:
        space_text(f"{stats()['wpm']} wpm")
    return state
WPM Spacebar добавляет переключатель в «Клавиши» > «Пробел». Пока он включён, на пробеле показывается текущая скорость набора.

Для плагинов нужен Clink Pro. Перед установкой плагина из репозитория Clink попросит разрешить код из этого репозитория, так же как для панелей и действий.

on_key и on_word получают то, что вы набираете: так работает, например, счётчик слов. У PyMini нет доступа к сети и файлам, поэтому плагин не может отправить набранный текст через интернет, а то, что он сохраняет, остаётся в Clink на вашем устройстве.

Ваш первый плагин

Быстрее всего начать со стартового скрипта, который приложение пишет за вас. Он ставит переключатель в «Клавиши» > «Пробел» и, пока тот включён, считает слова на пробеле. Пять шагов, ничего скачивать не нужно.

  1. Откройте вкладку «Плагины», включите плагины сверху и нажмите + для нового плагина.
  2. Редактор откроется на стартовом скрипте. Прочитайте его один раз: initial(), settings(), on_action(), on_open() и on_word() это всё.
  3. Переключитесь на предпросмотр. Включите переключатель, затем несколько раз нажмите on_open и on_word и следите за макетом пробела и консолью.
  4. Сохраните. Плагин включён в списке, а его переключатель теперь есть и в «Клавиши» > «Пробел».
  5. Откройте клавиатуру где угодно и печатайте. Пробел считает вместе с вами.
def initial():
    return {"on": False, "words": 0}

def settings(state):
    return section("keys.spacebar", [
        toggle("Count words on the space bar", state["on"], action="toggle"),
    ], title="Word count")

def on_action(action, value, state):
    if action == "toggle":
        state["on"] = value
        if value:
            claim("spacebar.text")
        else:
            release("spacebar.text")
            space_text(None)
    return state

def on_open(state):
    state["words"] = 0
    if state["on"]:
        space_text("0 words")
    return state

def on_word(word, state):
    state["words"] += 1
    if state["on"]:
        space_text(f"{state['words']} words")
    return state
Стартовый скрипт в том виде, в каком его пишет приложение.

Хуки

Определите нужные из этих 33 функций, остальные можно не писать. Каждая получает state последним аргументом. Те, что на что-то реагируют, возвращают state, изменённым или нет. Те, что отвечают на вопрос (elements, draw, effects, хуки оформления, haptics, hitboxes, suggestions и correct), возвращают свой ответ и при этом могут менять state на месте.

ХукКогда вызывается
on_automation(command, args, state)on_automation(command, args, state) выполняет разрешённую локальную команду в существующей песочнице. Обработчик может обновить подпись пробела, опубликовать объявленное состояние или вызвать объявленное событие. Вставка текста, доступ к буферу обмена, постоянное изменение настроек и внешние запросы в этом обработчике запрещены.
initial()Один раз, пока сохранённого состояния ещё нет. Верните dict с любыми данными, которые можно сохранить в JSON.
settings(state)Когда настройки плагина показываются в приложении. Верните его элементы управления в виде дерева узлов. В клавиатуре никогда не выполняется.
on_action(action, value, state)Когда используется один из элементов управления плагина. Вариант on_action(action, state), без value, тоже работает.
on_open(state)Когда появляется клавиатура. Удобное место, чтобы прочитать stats() или задать пробел.
on_close(state)Когда клавиатура скрывается. Сразу после этого состояние сохраняется.
on_key(key, state)Каждый раз, когда клавиша что-то вводит. Выполняется при каждом нажатии, поэтому должен работать быстро.
on_word(word, state)Когда слово завершено: пробелом, подсказкой или свайпом.
on_backspace(state)Когда нажата клавиша удаления. Текста не получает, только состояние.
on_suggestion(word, state)Когда нажата подсказка. word это нажатое слово.
on_language(code, state)Когда меняется язык набора. code это новый язык, например en или de.
on_field(kind, state)Когда клавиатура подключается к полю. kind это default, email, url, number, phone, password или search.
on_tick(state)Раз в секунду, пока клавиатура на экране, печатаете вы или нет. Хук для всего, что должно меняться само: часы, обратный отсчёт или скорость, которая должна падать до нуля, когда вы остановились.
on_swipe(direction, state)Взмах, начатый на клавише буквы: "left", "right", "up", "up_left" или "up_right". Взмахи читаются, только пока выключен ввод свайпом. Если хук ничего не просит, взмах остаётся обычным нажатием; если просит, сначала убирается буква, с которой он начался.
elements(state)Что этот плагин предлагает пользовательской раскладке. Верните записи element(id, name, icon=, width=) или не определяйте хук.
draw(id, state)Вид одного элемента в виде дерева узлов. Выполняется примерно раз в секунду, пока клавиатура на экране.
effects(state)Эффекты подсветки, которые этот плагин предлагает странице «Эффекты». Верните записи light_effect(...) или не определяйте хук.
key_styles(state)Стили клавиш для редактора тем. Возвращайте записи key_style(...); см. раздел «Оформление» ниже.
themes(state)Готовые темы для вкладки «Плагины» в редакторе тем. Возвращайте записи theme(...).
popups(state)Стили всплывающего окна клавиши. Возвращайте записи popup_style(...).
animations(state)Анимации: любое сочетание entrance(...), press_animation(...), letter_animation(...) и transition(...).
backgrounds(state)Анимированные фоны. Возвращайте записи background(...) из слоёв particles(...).
layouts(state)Раскладки клавиатуры. Возвращайте записи layout(...).
trails(state)Следы свайпа, которые плагин предлагает выбору следа. Верните записи trail(...); см. «Оформление» ниже.
haptics(state)Тактильная отдача для каждой клавиши: dict из имён клавиш в ощущения. Читается при открытии клавиатуры и примерно раз в секунду.
hitboxes(state)Зона нажатия для каждой клавиши: dict из имён клавиш в hitbox(...). Читается в том же ритме, что и haptics.
on_touch(key, x, y, state)После каждого нажатия: клавиша, которой оно досталось, и место на ней, куда опустился палец. x и y идут от -0.5 до 0.5, 0 в центре.
suggestions(word, state)Слова для панели подсказок, пока набирается word. Вызывается каждый раз, когда панель успокаивается, а не на каждую клавишу.
correct(word, fix, state)Пробел только что завершил word. Верните слово, чтобы вставить его вместо набранного, False, чтобы оставить как набрано, или None, чтобы действовало исправление клавиатуры.
bar_items(state)Кнопки и регуляторы, которые плагин предлагает верхней панели. Верните записи bar_button(...) и bar_knob(...) или не объявляйте хук.
key_art(state)Рисунок для клавиш: dict из имён клавиш в фигуры. Перечитывается после каждого события, которое слышит плагин, так что изменённое в хуке состояние тут же видно на клавишах.
events(state)Имена событий, которые хочет слышать on_event; читаются один раз при загрузке плагина. Без этого хука плагин слышит всё, кроме частых событий.
on_event(name, info, state)Один хук на всё, что происходит: open, close, word, backspace, suggestion, language, field, shift и plane, а также частые key, key_down, key_up, predictions и tick, которые нужно запросить по имени в events(state).
def on_key(key, state):
    if key == " " and setting("language") == "en":
        state["spaces"] = state.get("spaces", 0) + 1
    return state

def on_close(state):
    haptic("light")
    return state
Два хука, одно чтение и команда панели.

У приложения и клавиатуры одна общая копия состояния. Включите переключатель в приложении, и он будет включён при следующем открытии клавиатуры. Всё, что насчитала клавиатура, будет на месте, когда вы в следующий раз откроете настройки плагина. Клавиатура сохраняет состояние при закрытии, а не после каждой клавиши.

Текущая подсказка на клавише пробела

Читайте основную показанную подсказку через context()["suggestion"]. Подпишитесь на событие predictions, чтобы получать изменения в info["suggestion"]. Для обоих способов нужен доступ к данным ввода. on_suggestion вызывается после выбора подсказки, а не при обновлении вариантов. Вызов space_text(value or None) задаёт подпись или восстанавливает её, когда подсказок нет. Действие клавиши пробела не меняется.

def initial():
    return {}

def events(state):
    return ["predictions"]

def on_open(state):
    space_text(context()["suggestion"] or None)
    return state

def on_event(name, info, state):
    if name == "predictions":
        space_text(info["suggestion"] or None)
    return state

def on_close(state):
    space_text(None)
    return state

Состояние

Состояние это один dict. initial() строит его в первый раз; после этого каждый хук получает тот же dict, меняет его и возвращает. В нём хранится всё, что умещается в JSON: числа, строки, списки, вложенные dict. Приложение сохраняет его после каждого использованного элемента, клавиатура при закрытии, и оба читают один файл, так что они никогда надолго не расходятся.

def initial():
    return {"session": 0, "total": 0, "longest": ""}

def on_open(state):
    state["session"] = 0        # starts over each time the keyboard opens
    return state

def on_word(word, state):
    state["session"] += 1
    state["total"] += 1         # survives, it is saved when the keyboard closes
    if len(word) > len(state["longest"]):
        state["longest"] = word
    return state

def settings(state):
    return vstack([
        text(f"{state['total']:,} words so far"),
        text(f"Longest: {state['longest'] or '...'}", size=13, color="gray"),
        button("Start over", "reset", style="destructive"),
    ])

def on_action(action, value, state):
    if action == "reset":
        return initial()
    return state
Счётчик за сессию, который on_open сбрасывает, и сохраняющийся итог.

Возвращать state полезная привычка, но не строгое требование: dict это ссылка, так что менять его на месте тоже работает. Верните другой dict, как initial() выше, и он станет состоянием.

Настройки и разделы

settings(state) возвращает дерево узлов, собранное из конструкторов панелей: text, toggle, slider, stepper, segmented, button, row, field и контейнеров компоновки. Оно появляется на странице плагина на вкладке «Плагины». Оберните часть дерева в section(anchor, children, title), и эта часть появится также на одном из экранов настроек самого Clink, рядом с настройкой, к которой она относится.

def settings(state):
    return vstack([
        text("Counts words as you type.", size=13),
        section("keys.spacebar", [
            toggle("Count on the space bar", state["on"], action="toggle"),
            stepper(state["goal"], min=10, max=500, step=10, label="Goal", key="goal"),
        ], title="Word count"),
    ])
Переключатель и степпер появляются в «Клавиши» > «Пробел». Подпись видна только на странице самого плагина.

Элементы, которые плагину нужны чаще всего. Каждый либо записывает новое значение в состояние под key, либо называет action для on_action, либо и то и другое. Полный список построителей, включая раскладки, на странице панелей.

КонструкторЧто рисует
toggle(label, on=False, key="", action="")Переключатель. key записывает True или False в этот ключ состояния.
slider(value, min=0, max=100, step=1, label="", key="", action="")Ползунок между min и max. key записывает положение в этот ключ состояния.
stepper(value, min=0, max=100, step=1, label="", key="", action="")Значение с кнопками − и + рядом, в пределах от min до max.
segmented(options, value=None, key="", action="")Один вариант из списка. key записывает выбранный вариант в этот ключ состояния.
field(key, placeholder="", action="", submit="")Текстовое поле, привязанное к state[key]. Касание направляет клавиши в этот ключ; submit называет обработчик, который запускает Return.
button(label, action="", value=None, insert="", set=None, style="plain", icon="", enabled=True)Кнопка. insert печатает свой текст туда, где вы пишете, set вносит ключи в состояние, а action называет обработчик для on_action. style принимает plain, primary, tinted, quiet или destructive.
row(title, subtitle="", detail="", icon="", action="", value=None, insert="")Строка, по которой можно нажать: заголовок, вторая строка, значок и деталь справа.
text(s, size=17, weight="regular", color="", align="leading", lines=0, mono=False)Строка текста. weight принимает regular, medium, semibold, bold, heavy, light или thin; align принимает leading, center или trailing; lines ограничивает число строк при переносе; color принимает имя цвета или #RRGGBB.
Панели ›

Куда попадают разделы

Якорь это идентификатор карточки на одном из собственных экранов настроек Clink. Назовите его в section(), и элементы плагина будут нарисованы прямо под этой карточкой, с именем плагина над ними. Проще всего найти его так: в приложении откройте «Ещё» > «Разработчик» и включите «Показывать идентификаторы». У каждой карточки появится маленький значок с её идентификатором, нажатие копирует его; страницы показывают свой в заголовке.

Все якоря, которые может назвать раздел

  • analytics.heatmap
  • analytics.privacy
  • analytics.trends
  • analytics.typing-test
  • automations.rules
  • gestures.accents
  • gestures.cursor
  • gestures.delete
  • gestures.general
  • gestures.suggestions
  • gestures.swipe
  • haptics.feel
  • keys.adaptive
  • keys.faces
  • keys.hitboxes
  • keys.hitmap
  • keys.long-press
  • keys.numberrow
  • keys.onehanded
  • keys.roundness
  • keys.size
  • keys.spacebar
  • keys.spacing
  • keys.split
  • languages.app
  • languages.custom
  • languages.manage
  • languages.packs
  • languages.switch
  • languages.typing
  • layout.arrange
  • layout.arrangement
  • layout.build
  • layout.longpress
  • layout.presets
  • layout.topbar
  • motion.delete
  • motion.entrance
  • motion.glow
  • motion.key-press
  • motion.key-response
  • motion.letters
  • motion.space-response
  • motion.transition
  • popups.style
  • sound.keysounds
  • text.automation
  • text.content
  • text.corrections
  • text.history
  • text.punctuation
  • text.speed
  • text.suggestions
  • text.symbols
  • themes.background
  • themes.canvas
  • themes.theme

У keys.spacebar есть собственное место на самом экране пробела. Раздел с якорем, которого нет в этом списке, всё равно показывается на странице плагина, так что опечатка стоит вам карточки, а не плагина.

Занятые элементы

Когда плагин управляет одной из настроек Clink, человек должен это видеть, и они не должны спорить. claim(control) говорит, что плагин владеет ей: приложение называет плагин на карточке этой настройки, а поле текста пробела блокируется, пока плагин её держит. release(control) возвращает её. Занимайте в том on_action, который включает вашу функцию, отпускайте в том, который выключает, и одновременно возвращайте значение в None или к прежнему. Плагин, выключенный в списке, отпускает всё, что удерживал.

def on_action(action, value, state):
    if action == "toggle":
        state["on"] = value
        if value:
            claim("spacebar.text")
            space_text("...")
        else:
            release("spacebar.text")
            space_text(None)      # hand the person's own text back
    return state
Пара, которая есть у каждого занимающего плагина: взять при включении, вернуть при выключении.

Два плагина могут занять один элемент; приложение назовёт тот, что сделал это последним. Занятость записывает приложение, поэтому занимайте из on_action, который выполняется там. Занятость, сделанная внутри клавиатуры, не запоминается.

Команды и чтения

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

ВызовЧто делает
space_text(text)Показывает подпись на пробеле, до 32 символов. Передайте None, чтобы вернуть текст пробела, заданный пользователем.
space_language_text(text)Переопределяет языковой значок в углу пробела, сам по себе и даже когда штатный значок выключен. Одна строка, до 12 символов; "" прячет его, а None возвращает штатный текст.
space_language_flag(language)Ставит на этот значок флаг для идентификатора языка вроде en_GB, из встроенной графики флагов. None возвращает штатное поведение.
space_language_emoji(language)Тот же значок в виде эмодзи флага региона. Три команды значка делят одно место, так что включение одного плагина со значком выключает остальные.
set_setting(name, value)Меняет одну из настроек Clink по имени на значение нужного вида: логическое, число в допустимом диапазоне, один из вариантов или текст. Неизвестное имя вызывает KeyError. Значение не того вида или вне диапазона не применяется, а консоль редактора показывает, что ожидает настройка.
claim(control)Забирает одну из собственных настроек Clink. На её карточке в приложении указан плагин, который её держит.
release(control)Возвращает настройку обратно. При выключении плагина освобождается всё, что он занял.
suggest(words)Ставит до десяти своих слов в начало панели подсказок. Они остаются, пока одно не нажато, не нажато удаление или не сменилось поле, а suggest([]) убирает их раньше. Нажатие вводит слово.
banner(text)Ненадолго показывает короткое сообщение на клавиатуре. Для подсказки, а не для разговора.
press(key)Нажимает одну из собственных клавиш клавиатуры, как будто по ней коснулись: "space" или "delete". Любое другое имя вызывает ValueError.
pick_suggestion(slot)Выбирает подсказку в части строки "left", "center" или "right", так же как касание по ней. Если строка прокручена, считается то, что видно на экране. Внутри on_swipe выбор идёт из строки в том виде, в каком она была, когда палец коснулся клавиши.
stats()Возвращает dict с wpm, peak_wpm, keystrokes, words и streak. wpm обновляется в реальном времени, пока плагины включены. Итоги берутся из «Аналитики» и перестают обновляться, если «Аналитика» выключена.
setting(name)Читает по имени одну из настроек, перечисленных ниже. Любое другое имя вызывает KeyError.
context()Тот же снимок, который читает панель, плюс suggestion, shift (off, on или locked) и plane для плагина. Ключи документа наполняет доступ к данным набора; shift читается и без него.
КлючЧто содержит
stats()["wpm"]Слов в минуту за последние несколько секунд, по символам, которые дошли до поля. Число появляется через секунду-другую после начала, падает во время паузы и доходит до 0, когда вы остановились. Удаление его никогда не увеличивает.
stats()["peak_wpm"]Лучшая скорость, когда-либо записанная аналитикой.
stats()["keystrokes"]Нажатий клавиш за всё время, как их считает аналитика.
stats()["words"]Завершённых слов за всё время.
stats()["streak"]Дней подряд с набором текста, заканчивая сегодняшним.

setting(name) и set_setting(name, value) используют один список имён, и claim(control) тоже принимает любое из них. Логические значения читаются как True или False, числа как числа, варианты по их id. Список длинный намеренно: плагин может реагировать почти на всё, что человек может настроить в приложении, или управлять этим.

Имена, которые принимает setting()

  • analytics
  • emoji.skin_tone
  • emoji.trailing_space
  • gestures.cursor
  • gestures.cursor_style
  • gestures.highlight_shift
  • gestures.plane_slide
  • gestures.predictive_flick
  • gestures.quick_accent
  • gestures.swipe
  • gestures.swipe_delete
  • gestures.swipe_multi_word
  • gestures.swipe_space_commit
  • gestures.swipe_two_thumb
  • gestures.trail
  • gestures.trail_style
  • gestures.trail_width
  • haptics.enabled
  • haptics.intensity
  • haptics.sharpness
  • keys.accents
  • keys.glyph_scale
  • keys.height
  • keys.long_press_hint
  • keys.popup_style
  • keys.popups
  • keys.radius
  • keys.row_spacing
  • keys.spacing
  • keys.uppercase
  • keys.width
  • language
  • language.bar_key
  • language.bar_key_style
  • language.mode
  • layout
  • layout.dismiss_shortcut
  • layout.number_row
  • layout.number_row_scale
  • layout.one_handed
  • layout.one_handed_shortcut
  • layout.one_handed_side
  • layout.one_handed_width
  • layout.split
  • layout.split_gap
  • layout.split_number_row
  • layout.split_shortcut
  • layout.split_space_bar
  • pet.corner
  • pet.enabled
  • pet.species
  • settings.accentHoldDelay
  • settings.accentMoveCancel
  • settings.activateWithIcon
  • settings.adaptiveGrow
  • settings.adaptiveHitboxes
  • settings.adaptivePredictAtWordStart
  • settings.adaptivePredictionWeight
  • settings.adaptiveShrink
  • settings.adaptiveSpace
  • settings.aiAutocorrect
  • settings.aiCompletions
  • settings.aiExtensionEnabled
  • settings.aiSearch
  • settings.aiToolsColorOverrides
  • settings.aiToolsDiffStyle
  • settings.aiToolsDisabled
  • settings.aiToolsLayoutStyle
  • settings.aiToolsOrder
  • settings.aiToolsPromptOverrides
  • settings.aiTranslate
  • settings.alternatingSplit
  • settings.arabicIndicNumerals
  • settings.backgroundEffectOverride
  • settings.clipboardCloseOnPaste
  • settings.clipboardDeleteOnPaste
  • settings.clipboardIgnoreImages
  • settings.clipboardIgnorePinsOnDelete
  • settings.clipboardStyle
  • settings.cursorActivationHaptic
  • settings.cursorLineStride
  • settings.cursorStepHaptic
  • settings.customPanelsStandalone
  • settings.customPetID
  • settings.deleteWordSwipeEngage
  • settings.deleteWordSwipeStride
  • settings.dictationAssist
  • settings.dictationAssistCustom
  • settings.dictationAssistLevel
  • settings.dictationColorScheme
  • settings.dictationGlassCapsule
  • settings.dictationSmartActions
  • settings.dictationStyle
  • settings.dictationVisualStyle
  • settings.dragUpThreshold
  • settings.emojiCategoryOrder
  • settings.emojiCellSpacing
  • settings.emojiColumnCount
  • settings.emojiCrossAxisSwitchesTab
  • settings.emojiCustomSets
  • settings.emojiGlyphScale
  • settings.emojiHiddenCategories
  • settings.emojiHiddenFromPanels
  • settings.emojiRecentsCap
  • settings.emojiRecentsSort
  • settings.emojiRememberCategory
  • settings.emojiRowCount
  • settings.emojiScrollDirection
  • settings.emojiSearchSlot
  • settings.emojiShowABCKey
  • settings.emojiShowBackspaceKey
  • settings.emojiStartCategoryID
  • settings.emojiTabBarSlot
  • settings.emojiTabIconStyle
  • settings.emojiToneHoldDelay
  • settings.extensionOrder
  • settings.extraTopBars
  • settings.formFreeCorners
  • settings.formLayoutEnabled
  • settings.gifShareAsLink
  • settings.glassPerRowMerge
  • settings.glassReleaseResponse
  • settings.gridSwitchAnimation
  • settings.gridSwitchDuration
  • settings.handwritingInkColor
  • settings.handwritingInkGlow
  • settings.handwritingInkStyle
  • settings.handwritingInkWidth
  • settings.hitboxScale
  • settings.iconPickerStyle
  • settings.keyBloomScale
  • settings.keyLighting
  • settings.keyPressGlow
  • settings.keyPressInstant
  • settings.keyPressLinger
  • settings.keySpringDamping
  • settings.keySpringResponse
  • settings.keyboardBottomPadding
  • settings.keyboardLanguages
  • settings.keyboardTopPadding
  • settings.longPressGlyphScale
  • settings.minPressVisible
  • settings.notepadBrowseStyle
  • settings.notepadMode
  • settings.numberRowFontSize
  • settings.numberRowLeadingKeys
  • settings.numberRowTrailingKeys
  • settings.oneHandedCustomKeys
  • settings.oneHandedExtraKeys
  • settings.panelButtonHitboxScale
  • settings.persistentLeadingKeys
  • settings.persistentTrailingKeys
  • settings.petIdleMotion
  • settings.petScale
  • settings.pinyinFuzzyEnabled
  • settings.pluginLooks
  • settings.popupSpringDamping
  • settings.popupSpringResponse
  • settings.predictiveFlickSuggestionPosition
  • settings.predictiveFlickSuggestionsSeparate
  • settings.reduceEffectsOnLowPower
  • settings.repeatAccelStep
  • settings.repeatHoldDelay
  • settings.repeatInitialInterval
  • settings.repeatMinInterval
  • settings.replacementsLayout
  • settings.rowInsets
  • settings.secondaryNeuralModelsEnabled
  • settings.separateActivation
  • settings.separateLanguageLayouts
  • settings.showCaptureOverlay
  • settings.showCorrectionField
  • settings.showHitboxOverlay
  • settings.showIconsBeforeTyping
  • settings.showRecentEmoji
  • settings.showTouchHeatmap
  • settings.showTouchSurfaceBounds
  • settings.showTouchTelemetryOverlay
  • settings.slideUpPickerStyle
  • settings.solidPopupOpacity
  • settings.soundVoice
  • settings.spaceBloomScale
  • settings.spaceCursorActivationDelay
  • settings.spaceCursorDragScale
  • settings.spaceCursorStride
  • settings.spaceLeanMultiplier
  • settings.spaceSpringDamping
  • settings.spaceSpringResponse
  • settings.spatialBiasEnabled
  • settings.spatialBiasGain
  • settings.splitIndices
  • settings.splitOtherPlanes
  • settings.stickerGridSnap
  • settings.stickerPlacements
  • settings.suggestionDebounceDelay
  • settings.suggestionHitboxScale
  • settings.suggestionSegmentLiquidGlass
  • settings.suggestionSegmentStyle
  • settings.suggestionSegmentsFollowTheme
  • settings.suggestionSeparatorColor
  • settings.suggestionSeparatorStyle
  • settings.suggestionTopPadding
  • settings.swipeKeyMorph
  • settings.swipeMorphRadius
  • settings.swipeMorphStrength
  • settings.swipeTrailEndWidth
  • settings.swipeTrailMaxLength
  • settings.swipeTrailStartWidth
  • settings.swipeTrailTrim
  • settings.toolsButtonStyle
  • settings.toolsHiddenFromPanels
  • settings.topBarItems
  • settings.topBarOrder
  • settings.translateLanguageOrder
  • settings.translateLanguagesDisabled
  • settings.translateStyle
  • settings.translateTone
  • settings.vietnameseInputMethod
  • sound.enabled
  • sound.pack
  • sound.volume
  • spacebar.corner
  • spacebar.language_code
  • spacebar.size
  • spacebar.text
  • stickers.enabled
  • text.arithmetic
  • text.auto_capitalize
  • text.auto_punctuation
  • text.autocorrect
  • text.autocorrect_everywhere
  • text.contacts
  • text.conversions
  • text.double_space_period
  • text.learning
  • text.punctuation_spacing
  • text.return_to_letters
  • text.revert_on_delete
  • text.smart_quotes
  • text.suggestions
  • text.suggestions_animation
  • text.suggestions_height
  • text.suggestions_scroll
  • text.suggestions_style
  • theme
  • theme.background
  • theme.dark
  • theme.delete_glyph
  • theme.entrance
  • theme.glyph_press
  • theme.light
  • theme.match_system
  • theme.press_style
  • theme.reactive_background
  • tools.calculator
  • tools.cling
  • tools.clipboard
  • tools.conversion
  • tools.dictation
  • tools.dictionary
  • tools.emoji
  • tools.gif
  • tools.handwriting
  • tools.layout_switcher
  • tools.notepad
  • tools.plugin_switcher
  • tools.profiles
  • tools.replacements
  • tools.textfx
  • tools.theme_switcher
  • tools.translate

Команды

  • insert()
  • backspace()
  • delete_word()
  • replace()
  • move_cursor()
  • copy()
  • close()
  • haptic()
  • toast()

Как и view() у панели, settings() должна только описывать элементы управления. Команды, вызванные из неё, игнорируются и записываются в консоль.

Примеры

Пять небольших плагинов, каждый законченный. Вставьте один в новый плагин, сохраните, и он работает.

def initial():
    return {"on": True}

def settings(state):
    return section("keys.spacebar", [
        toggle("Language on the space bar", state["on"], action="toggle"),
    ], title="Language badge")

def on_action(action, value, state):
    if action != "toggle":
        return state
    state["on"] = value
    if value:
        claim("spacebar.text")
        space_text(setting("language").upper())
    else:
        release("spacebar.text")
        space_text(None)
    return state

def on_open(state):
    if state["on"]:
        space_text(setting("language").upper())
    return state

def on_language(code, state):
    if state["on"]:
        space_text(code.upper())
    return state
Значок языка: язык набора на пробеле, обновляется в момент смены.
SHORTCUTS = {"omw": "on my way", "brb": "be right back", "ty": "thank you"}

def on_key(key, state):
    if key != " ":
        return state
    words = context()["before"].split()
    if words and words[-1] in SHORTCUTS:
        # the space is already in the text, so it goes too and comes back after
        replace(len(words[-1]) + 1, SHORTCUTS[words[-1]] + " ")
    return state
Сокращения: наберите omw и пробел, получите on my way. on_key видит пробел уже после ввода, поэтому замена перешагивает через него.
def initial():
    return {"goal": 200, "count": 0}

def settings(state):
    return vstack([
        stepper(state["goal"], min=50, max=2000, step=50, label="Words per session", key="goal"),
        progress(state["count"], total=state["goal"], label=f"{state['count']} of {state['goal']}"),
    ])

def on_open(state):
    state["count"] = 0
    return state

def on_word(word, state):
    state["count"] += 1
    if state["count"] == state["goal"]:
        haptic("medium")
        banner("Goal reached")
    return state
Цель по словам: степпер на странице плагина, индикатор прогресса, а по достижении цели вибрация и баннер.
def initial():
    return {"muted": False, "was_on": True}

def on_field(kind, state):
    # a password or a number field is not a place for key sounds
    quiet = kind in ("password", "number", "phone")
    if quiet and not state["muted"]:
        state["was_on"] = setting("sound.enabled")
        state["muted"] = True
        set_setting("sound.enabled", False)
    elif not quiet and state["muted"]:
        state["muted"] = False
        if state["was_on"]:
            set_setting("sound.enabled", True)
    return state
Тихие поля: звуки клавиш выключаются в поле пароля или числа и возвращаются потом, через on_field, setting() и set_setting().
NAMES = ["sam", "ana", "team"]

def on_key(key, state):
    typing = context()["before"].split(" ")[-1]
    if typing.startswith("@"):
        start = typing[1:].lower()
        suggest([n for n in NAMES if n.startswith(start)])
        state["showing"] = True
    elif state.get("showing"):
        suggest([])                 # the mention is over, give the bar back
        state["showing"] = False
    return state
Упоминания: ввод @ помещает ваши имена в строку подсказок.
def on_swipe(direction, state):
    if direction == "left":
        delete_word()
    elif direction == "right":
        press("space")              # the space bar's own path, so it autocorrects
    elif direction == "up":
        pick_suggestion("center")
    elif direction == "up_left":
        pick_suggestion("left")
    elif direction == "up_right":
        pick_suggestion("right")
    return state
Взмахи в стиле Fleksy, основа плагина Flick Gestures: влево удаляет слово, вправо ставит пробел, вверх выбирает подсказку. Взмахи приходят, только пока выключен ввод свайпом.

Элементы в раскладке

Пользовательская раскладка собирается из клавиш и элементов: полоса недавних эмодзи, ряд цифр, курсорная площадка. Плагин может добавить свои. elements(state) сообщает, что он предлагает, а draw(id, state) рисует элемент, так что клавиша может показывать всё, что плагин умеет вычислять: график скорости набора, часы, обратный отсчёт или собственный индикатор заряда.

Элемент собирают два хука. elements(state) перечисляет, что вы предлагаете, по одной записи на элемент, и приложение читает это для палитры редактора раскладок. draw(id, state) получает один из этих идентификаторов и возвращает, что рисовать. Плагин может предложить несколько; draw спрашивают о каждом по имени.

ВызовЧто делает
element(id, name, icon="", width=2)Один предлагаемый элемент. width измеряется в ширинах клавиши, как их считает раскладка, а icon это SF Symbol, который редактор показывает в палитре.
sparkline(values, min=None, max=None, fill=False)Линия по ряду чисел, заполняющая отведённое место. Без min и max она подстраивается под свои значения. Рисовать её может только плагин, и сделана она для клавиши.
POINTS = 30

def initial():
    return {"history": []}

def elements(state):
    return [element("sparkline", "WPM sparkline", icon="waveform.path.ecg", width=3)]

def on_tick(state):
    history = state["history"]
    history.append(stats()["wpm"])
    state["history"] = history[-POINTS:]
    return state

def draw(id, state):
    rate = state["history"][-1] if state["history"] else 0
    return hstack([
        sparkline(state["history"], min=0, max=max(60, max(state["history"] or [0])), fill=True),
        text(f"{rate}", size=12, weight="semibold"),
    ], spacing=5, align="center")
Плагин WPM Sparkline целиком: он предлагает один элемент и рисует на нём последние тридцать секунд вашей скорости.

Вид элемента это дерево узлов, как у страницы настроек, но клавиша не страница настроек: используются только рисующие узлы, а всё нажимаемое игнорируется. Клавиша уже принадлежит раскладке, поэтому внутри неё нет места для кнопки.

ЧТО МОЖЕТ РИСОВАТЬ ЭЛЕМЕНТ

  • sparkline
  • text
  • badge
  • icon
  • progress
  • hstack
  • vstack
  • spacer
  • divider

Рассчитывайте на клавишу. width измеряется в ширинах клавиши: 1 это буква, 3 примерно треть ряда, и человек потом может изменить размер. Текст занимает одну строку и ужимается, чтобы поместиться; цвета по умолчанию берутся от цвета текста клавиши, так что элемент подходит к соседним клавишам, пока вы не попросите другой. Sparkline хранит последние 120 точек, больше, чем клавиша способна показать.

import time

def elements(state):
    return [element("clock", "Clock", icon="clock", width=2)]

def draw(id, state):
    return text(time.format("HH:mm"), size=15, weight="semibold")
Часы на клавише: ни состояния, ни хуков, просто время на каждый запрос. Элементы сами перерисовываются примерно раз в секунду.
GOAL = 200

def initial():
    return {"words": 0}

def elements(state):
    return [element("goal", "Word goal", icon="target", width=2)]

def on_open(state):
    state["words"] = 0
    return state

def on_word(word, state):
    state["words"] += 1
    return state

def draw(id, state):
    return vstack([
        text(f"{state['words']}/{GOAL}", size=11),
        progress(state["words"], total=GOAL),
    ], spacing=2)
Цель по словам за сессию: считает on_word, а рисуется она полосой. Состояние ведут хуки, draw только читает его.

Элементы живут в пользовательских раскладках, так что сперва нужно собрать раскладку. Готовые её не удержат: скопировать готовую раскладку в свою это первое, что предложит редактор.

  1. Установите плагин и включите его. Его элементы появятся сразу же.
  2. Откройте «Раскладка», создайте или скопируйте свою раскладку и перейдите в «Расположение».
  3. Нажмите «Элемент», выберите элемент плагина в полосе и задайте ширину.

Предпросмотр в редакторе рисует каждый элемент, который предлагает скрипт, примерно в размер клавиши, где он окажется, под макетом пробела. Сохраните, разместите один раз, и клавиатура нарисует то же самое.

Элемент показывает только то, что рисует плагин: нажатие на него ничего не делает, потому что клавиша уже принадлежит раскладке. Раскладка хранит элемент, даже когда плагин выключен или удалён, и клавиша снова заполняется, когда он возвращается.

Кнопки и регуляторы в верхней панели

Полоса над клавишами собирается в разделе «Раскладка» > «Верхняя панель», и плагин может предложить для неё свои элементы управления. bar_items(state) их возвращает: bar_button(...) для нажатия, bar_knob(...) для вращения. Список читается при открытии клавиатуры и заново после каждого нажатия, поэтому элемент может появляться и исчезать вместе с состоянием плагина.

ВызовЧто делает
bar_items(state)Всё, что этот плагин предлагает панели, списком. id должен быть уникален только внутри плагина; панель хранит его рядом с id самого плагина. При повторе id остаётся первый, а элемент, который хук перестал возвращать, больше не рисуется.
bar_button(id, name, icon="", title="")Кнопка. name показывается в редакторе и читается VoiceOver. icon это SF Symbol, а title до 12 символов подписи рядом: со значком и без title остаётся один значок, без значка остаётся один текст. У кнопки нет ни значения, ни диапазона. Нажатие вызывает on_action(id, None, state), и всё, что кнопка делает, происходит там.
bar_knob(id, name, icon="", min=0, max=1, step=0, value=0, setting=None, art=None, rotor=None)Регулятор. min и max задают диапазон, step при значении больше нуля делает ход ступенчатым, а value говорит, откуда начать. Вместо этого setting= привязывает его к числовой настройке самого приложения, и тогда диапазон и положение берутся оттуда. art= и rotor= рисуют его на Python.
def initial():
    return {"width": 2.0}

def bar_items(state):
    return [
        bar_button("sig", "Sign off", icon="signature", title="Sign off"),
        bar_knob("volume", "Key volume", icon="speaker.wave.2", setting="sound.volume"),
        bar_knob("width", "Trail width", icon="scribble", min=1, max=6, step=0.5, value=state["width"]),
    ]

def on_action(action, value, state):
    if action == "sig":
        insert(" sent from Clink")
    elif action == "width":
        state["width"] = value
    return state
Кнопка и два регулятора, один из них привязан к громкости клавиш

И то и другое возвращается через on_action(action, value, state) по id элемента, так что элементы панели и настройки плагина делят один хук. Нажатие присылает None. Регулятор присылает значение на каждом щелчке, пока палец не отпущен, и ещё раз при отпускании, так что плагин может следить за движением или дождаться последнего значения.

Регулятор с setting= крутит значение, которое у приложения уже есть, из списка «Команды и чтения» выше, например sound.volume или haptics.intensity. Имя, которого приложение не знает, даёт KeyError в консоли. Диапазон и начальное положение берутся из этой настройки, собственная копия клавиатуры меняется прямо во время перетаскивания, так что следующее нажатие уже звучит на новом уровне, а значение сохраняется при отпускании. Регулятор громкости или отдачи, поднятый с нуля, на время жеста снова включает звук или вибрацию, чтобы щелчки были слышны и ощутимы по пути вверх; сохраняет этот переключатель сам плагин вызовом set_setting в on_action.

Регулятор можно нарисовать на Python. art= это неподвижная часть, оправа или корпус, а rotor= вращающаяся лицевая сторона: -135 градусов на минимуме и +135 градусов на максимуме. Оба принимают те же фигуры, заливки, градиенты и тени, что и графика клавиш, до шестнадцати фигур в каждом, а unit="key" масштабирует координаты к квадратному циферблату, где стрелка с отрицательным y смотрит вверх. Рисунку передаются цвет текста панели как "text" и акцент темы как "accent". Если опустить оба, регулятор будет кольцом со значком внутри; в редакторе уже стоящему в панели регулятору можно к тому же выбрать любую встроенную отделку.

Само по себе в панель ничего не попадает. Элемент ставят вручную, как меню и подсказки.

  1. Установите плагин и включите его. Его элементы панели появятся сразу.
  2. Откройте «Раскладка» > «Верхняя панель».
  3. Добавьте кнопку или регулятор плагина и перетащите на нужное место. Для регулятора можно ещё выбрать отделку.

Конструкторы молча пропускают незнакомое ключевое слово, без ошибки. setting=, min=, max=, step=, value=, art= и rotor= принадлежат только bar_knob, так что bar_button с любым из них всё равно соберётся как обычная кнопка, и ничего об этом не скажут: работа кнопки живёт в on_action. И наоборот, title= принадлежит кнопке, а регулятор его пропускает.

Эффекты подсветки

Плагин может добавить свою подсветку клавиш. effects(state) возвращает записи light_effect(...), и каждая появляется в приложении в разделе «Эффекты > Из плагинов», рядом со встроенными стилями и эффектами, которые люди создают сами. Эффект это данные, а не рисунок: стопка слоёв, которую клавиатура анимирует сама. Поэтому в скрипте ничего не выполняется на каждом кадре, а поверхности клавиш, буквы, свечение, вспышка при нажатии и сон в простое работают так же, как у встроенных стилей.

ВызовЧто делает
light_effect(id, name, layers=[...], colors=[], icon="")Один эффект. id должен быть уникальным только внутри плагина. layers это список light_layer(...), применяемых сверху вниз. colors это до восьми строк "#rrggbb", по которым цвет проходит по кругу; без них эффект следует настройке «Цвет» пользователя.
light_layer(pattern, ...)Один слой: узор из списка ниже. Все ключевые слова необязательны.
COLORS = ["#00e5ff", "#7c4dff", "#ff4081"]

def effects(state):
    return [
        light_effect("tide", "Tide", colors=COLORS, layers=[
            light_layer("solid", low=0.2, high=0.2),
            light_layer("wave", moves="both", speed=0.8, size=1.5,
                        direction="up"),
        ]),
    ]
Ровная основа, а поверх неё медленная волна через три цвета. Волна добавляет свой свет к основе и по ходу меняет цвет.

Узоры

  • solid
  • pulse
  • wave
  • gradient
  • twinkle
  • sweep
  • rain
  • flicker
  • checker
КлючЧто делает
moves="light"Что меняет узор: "light" яркость, "color" цвет или "both" и то и другое.
mix="add"Как его яркость сочетается со слоями выше: "add" складывает, "max" оставляет более яркий, "multiply" приглушает их как маска.
shape="smooth"Подъём и спад для pulse, wave и gradient: "smooth", "ramp", "step" или "spike".
direction="right"Куда движутся wave, gradient, sweep и rain: "right", "left", "down", "up" или "out" от центра.
speed=1От 0 до 4. При 0 узор стоит на месте.
size=1От 0,25 до 4: сколько раз узор повторяется по клавиатуре. Для twinkle задаёт, сколько клавиш горит, для sweep длину хвоста.
low=0, high=1Яркость, между которой движется узор, каждое значение от 0 до 1. Если low выше high, узор переворачивается.
color_span=1, color_offset=0Насколько далеко узор продвигается по цветам и откуда начинает, каждое значение от 0 до 1.
def effects(state):
    return [
        light_effect("scanner", "Scanner", layers=[
            light_layer("wave", moves="color", speed=0.3),
            light_layer("sweep", mix="multiply", speed=1.2, size=1.5),
        ]),
    ]
sweep в роли маски. Первый слой меняет только цвет, а sweep решает, какие клавиши горят. Без colors он следует настройке «Цвет», так что со «Спектром» получается радуга.

Выбранный эффект копируется в настройки пользователя, поэтому работает и с выключенным плагином. Пока плагин включён и один из его эффектов активен, клавиатура перечитывает effects(state) при открытии и затем примерно раз в секунду и подставляет то, что изменилось. Так эффект следует за состоянием, временем или stats().

def effects(state):
    # Rounded, so the effect only changes when the pace really does.
    tempo = round(0.3 + min(stats()["wpm"], 120) / 40, 1)
    return [
        light_effect("tempo", "Tempo", colors=["#ff3d7f", "#ffb000"], layers=[
            light_layer("wave", moves="both", speed=tempo, low=0.15),
        ]),
    ]
Tempo из плагина Light Show: волна, которая ускоряется, чем быстрее вы печатаете. Слой может менять скорость, и узор при этом не скачет.

Кнопка effects в редакторе показывает, что предлагает скрипт. Чтобы увидеть эффект в движении, сохраните плагин, выберите эффект в разделе «Эффекты > Из плагинов» и смотрите на клавиатуру вверху этого экрана.

Округляйте всё, что дёргается, например скорость печати. Эффект, который каждую секунду возвращается другим, каждую секунду заменяется впустую. Неизвестный узор или ключевое слово дают ValueError в консоли редактора, а эффект без слоёв пропускается.

Оформление

Семь хуков предлагают приложению варианты на выбор: key_styles, themes, popups, animations, backgrounds, trails и layouts. Каждый возвращает записи, собранные вызовами ниже, и они появляются в разделе «Из плагинов» рядом со встроенными вариантами. Исключение составляют темы: у них в редакторе тем своя вкладка «Плагины». Оформление состоит из чисел и слов, а не из кода рисования, поэтому в скрипте ничего не выполняется на каждом кадре. Выбранный вариант копируется в настройки человека, поэтому работает и с выключенным плагином, а выбор встроенного варианта после этого откладывает его в сторону.

ВызовЧто делает
key_style(id, name, material=, variant=, shape=, shadow=, cap=cap(...))Стиль клавиш, который применяется к теме, открытой в редакторе тем, с его карточки «Стиль». Меняются только названные поля: material, variant, shape, glass, fan, механические inner_radius, face_inset, edges, raised и light_angle, shadow (0 даёт плоские клавиши) и outline. Цвета остаются от темы.
cap(outline="round", corner=None, travel=2, layers=[cap_layer(...)])Нарисованная поверхность клавиши для стиля клавиш, собранная из записей cap_layer(...). outline это round или rect, corner переопределяет радиус, а travel говорит, насколько клавиша проседает при нажатии.
cap_layer(kind, paint, inset=0, x=0, y=0, blur=0, fade=None, when=[...])Один слой нарисованной поверхности. kind это fill, stroke или inner, paint принимает грамматику цвета клавиши, color(...) или gradient(...), а when ограничивает слой состояниями pale, dark, pressed, resting и highlighted. С moves=False слой остаётся на месте, пока клавиша проседает.
theme(id, name, background=, keys=, key_text=, style=key_style(...), ...)Готовая тема на вкладке «Плагины» редактора тем. background, keys и key_text обязательны, это цвета "#rrggbb"; special, special_text, accent, background_bottom (переход цвета книзу), dark, font и weight можно не указывать. style=key_style(...) задаёт отделку клавиш. Выбранная тема устанавливается как своя.
popup_style(id, name, shape="tile", width=48, height=56, lift=30, ...)Пузырь над нажатой клавишей, в разделе Вид > Всплывающие окна. shape это "tile", "round" или "balloon"; width, height, lift и font_size в пунктах, а response и damping задают пружину.
entrance(id, name, opacity=0, x=0, y=0, scale=1, tilt=0, spin=0, ...)Как появляется клавиатура, в разделе Вид > Появление. opacity, x, y, scale, tilt и spin это её начальное положение; к покою она приходит пружиной по response и damping.
press_animation(id, name, scale=, x=0, y=0, rotation=0, ...)Форма удерживаемой клавиши, в разделе Реакции > Геометрия: scale (или scale_x и scale_y), x, y и rotation при полном нажатии.
letter_animation(id, name, scale=, x=0, y=0, rotation=0, anchor="center")Короткий эффект, который буква проигрывает при каждом нажатии, в разделе Реакции > Буквы: те же числа в пике, плюс anchor.
transition(id, name, x=0, y=0, scale=1, tilt=0, fade=True, duration=None)Переключение между буквами, 123 и #+=, в разделе Вид > Переход. x и y это насколько уходят старые клавиши, в долях клавиатуры, а новые приходят зеркально. Ещё принимает scale, tilt, fade и duration.
background(id, name, layers=[...], colors=[])Анимированный фон, в разделе Вид > Фон: до четырёх слоёв particles(...) и до восьми цветов "#rrggbb".
particles(shape="glow", count=40, size=4, speed=20, direction="none", ...)Один слой фона. shape это "dot", "glow", "streak", "ring" или "square"; count, size, speed, direction, spread, gravity, wobble, life, twinkle и opacity задают движение, а burst выбрасывает частицы из каждой нажатой клавиши.
layout(id, name, rows=[...], left=[], right=[])Раскладка, в разделе Раскладка > Расположение. Каждый ряд это список клавиш: строка это буква, а layout_key(...) всё остальное. left и right ставят до трёх клавиш рядом с пробелом. Выбор устанавливает обычную пользовательскую раскладку.
layout_key(glyph, action="insert", width=1)Клавиша, которая не просто буква. action это одно из insert, spacer, shift, delete, space, return, numbers, emoji, globe, tab, left, right, undo, redo или dismiss, а width считается в клавишах.
trail(id, name, layers=[...], colors=[])След свайпа. layers это записи trail_line, trail_stamps и trail_head, рисуемые по порядку, а colors это палитра, к которой они обращаются.
trail_line(width=1, tail_width=1, color=-1, glow=0, dash=0, gap=0, band=0, flow=0)Штрих вдоль свайпа. tail_width утончает старый конец, color=-1 растягивает всю палитру вдоль линии, а dash, gap, band и flow разбивают её или приводят в движение.
trail_stamps(shape="dot", size=1, spacing=14, scatter=0, spin=0, twinkle=0, color=-1)Фигуры, падающие вдоль свайпа: dot, ring, square, diamond, star, spark или heart. spacing это расстояние между ними в пунктах, scatter разбрасывает их с линии, spin вращает, а twinkle заставляет мерцать.
trail_head(shape="dot", size=1.5, pulse=0, opacity=1, color=-1, glow=0)Метка под кончиком пальца. pulse заставляет её дышать, а glow рассеивает вокруг неё свет.
def animations(state):
    return [
        entrance("swoop", "Swoop", y=90, scale=0.94, tilt=-20,
                 response=0.5, damping=0.72),
        press_animation("dip", "Dip", scale=0.93, y=2),
        letter_animation("bounce", "Bounce", y=-7, scale=1.12,
                         anchor="bottom"),
        transition("glide", "Glide", x=0.3, scale=0.96),
    ]
По одной анимации каждого вида. Каждая появляется в «Из плагинов» на своей карточке в разделе «Вид».
def backgrounds(state):
    return [
        background("snowfall", "Snowfall", colors=["#ffffff", "#cfe8ff"], layers=[
            particles(shape="dot", count=70, size=2.2, speed=28,
                      direction="down", spread=12, wobble=10, life=9),
            particles(shape="glow", count=0, size=3, life=1,
                      burst=8, burst_speed=90),
        ]),
    ]
Snowfall: снежинки, падающие с покачиванием, и вспышка света из каждой нажатой клавиши.

Возвращайте узлы trail из trails(state). У каждого следа есть палитра colors и слои, например trail_line. При color=-1 цвета плавно переходят друг в друга вдоль свайпа. Включите плагин, затем выберите его след в разделе «Из плагинов» в списке следов. Само по себе сохранение графики в состоянии не рисует след.

def trails(state):
    return [trail("rainbow", "Rainbow", colors=["#ff0000", "#ff8800", "#ffff00", "#00cc44", "#0066ff", "#9900ff"],
        layers=[trail_line(width=1.3, tail_width=0.2, color=-1)])]
Радужный след свайпа

Оформление копируется в момент выбора, поэтому изменение скрипта потом не меняет уже выбранный вариант; его нужно выбрать снова. Ключевое слово с опечаткой, слово не из своего списка или текст на месте числа дают ошибку в консоли редактора, а числа удерживаются в тех же пределах, что и у регуляторов самого приложения.

Рисунок на клавишах

key_art(state) возвращает dict из имён клавиш в рисунок, и клавиатура рисует его внутри клавиши. Имена клавиш те же, что у haptics, с запасными letters и keys. Рисунок это список shape(...), одна фигура, слой art([...]) или список слоёв, и каждый слой переходит к следующему рисунку по своим часам, так что одна клавиша может нести две вещи, движущиеся с разной скоростью.

ВызовЧто делает
art(shapes, animate=0, curve="ease_out")Один анимируемый слой. Передайте новый рисунок, и слой перейдёт к нему за animate секунд по кривой curve: ease_out, linear, ease_in, ease_in_out или spring. Несколько слоёв на одной клавише анимируются каждый сам по себе.
shape(kind, anchor="center", unit="pt", x=0, y=0, size=, width=, height=, fill=, stroke=, ...)Одна нарисованная вещь: circle, rect, capsule, line, path, text или icon. Она стоит на x и y от точки anchor на клавише, в пунктах или, при unit="key", в долях клавиши. fill и stroke принимают hex-цвет, "text", "accent", color(...) или gradient(...), а ещё есть line_width, corner, trim_from, trim_to, rotation, opacity, blur и до трёх теней.
graph(values, min=None, max=None, width=0.8, height=0.25, stroke=, fill=)Числовой ряд, развёрнутый в обычные фигуры: присоедините его к списку фигур или используйте сам по себе. Хранятся последние 60 конечных значений, границы по умолчанию берутся из самого ряда, а fill добавляет замкнутый контур до базовой линии под линией.
color(value, opacity=1)Заливка: hex-цвет либо "text" или "accent", разрешаемые по клавише, на которую он попал, с прозрачностью opacity.
gradient(kind, colors, stops=[], start=, end=, center=, radius=0.5)Линейный или радиальный градиент по списку цветов. stops расставляет их, start и end задают направление линейного, center и radius ставят радиальный.
shadow(color, radius=4, x=0, y=0)Тень под фигурой, их может быть до трёх. Дымка, заполняющая клавишу, идёт по её углам, а не срезает их; свечение выходит за пределы клавиши.

Рисунок перечитывается после каждого события, которое слышит плагин, и в этом вся петля: меняете состояние в on_event(name, info, state) и рисуете из него в key_art(state). events(state) называет нужные события и читается один раз при загрузке плагина. Без него плагин слышит всё, кроме key, key_down, key_up, predictions и tick, которые запускают скрипт на каждое нажатие или каждую секунду и потому запрашиваются отдельно. Смена регистра и плоскости перерисовывает рисунок независимо от того, слушает ли кто-нибудь, и доходит также до context().

def initial():
    return {"shift": "off"}

def events(state):
    return ["shift"]

def on_event(name, info, state):
    state["shift"] = info["state"]
    return state

def key_art(state):
    if state["shift"] != "locked":
        return {}
    lamp = shape("circle", anchor="top_right", x=-7, y=7, size=5,
                 fill="accent", shadow=shadow("accent", radius=4))
    return {"shift": art([lamp], animate=0.12)}
Лампочка Caps Lock, нарисованная плагином, а не встроенная в клавиатуру

Фигуры

  • circle
  • rect
  • capsule
  • line
  • path
  • text
  • icon

Шестнадцать фигур на клавишу, считая все её слои, и 64 точки на контур; всё сверх этого отбрасывается. Фигура рисуется внутри коробки своих width и height, а коробка без толщины не рисует ничего, поэтому горизонтальной линии нужна своя небольшая height с точками по её середине (height=0.03, points=[[0, 0.5], [1, 0.5]]), иначе она молча не появится.

Тактильная отдача, зоны нажатия и исправления

haptics(state) и hitboxes(state) возвращают dict с именами клавиш в ключах: сама буква, "space", "delete", "return", "shift" или "globe", затем "letters" для любой не названной буквы и "keys" для всего остального. Клавиши, которые плагин не назвал, сохраняют собственные настройки человека. Обе таблицы читаются при открытии клавиатуры и примерно раз в секунду, так что ради них ничего не выполняется на каждое нажатие.

ВызовЧто делает
feel(style=None, intensity=None, sharpness=None)Тактильная отдача: style это "soft", "light", "medium", "heavy", "rigid" или "off", а intensity и sharpness (от 0 до 1) её подстраивают. Можно и просто слово стиля.
hitbox(scale=1, x=0, y=0)Зона нажатия: x и y сдвигают цель клавиши на эту долю её размера, не больше половины клавиши, а scale увеличивает или уменьшает её. Просто число означает scale.
def haptics(state):
    return {
        "space": "heavy",
        "return": "rigid",
        "delete": feel(intensity=0.45, sharpness=0.9),
    }
Heavy Space: более тяжёлый пробел, чёткий ввод и лёгкое удаление.

on_touch(key, x, y, state) вызывается после каждого нажатия, когда оно уже обработано, с местом на клавише, куда опустился палец. Положение отсчитывается от нарисованной клавиши, а не от её сдвинутой цели, поэтому плагин может сдвигать каждую клавишу туда, куда по ней на самом деле попадают, не гоняясь за собственным сдвигом.

def initial():
    return {"keys": {}}

def on_touch(key, x, y, state):
    if len(key) != 1:
        return state
    n, ax, ay = state["keys"].get(key, [0, 0.0, 0.0])
    n = min(n + 1, 50)
    ax += (x - ax) / n
    ay += (y - ay) / n
    state["keys"][key] = [n, ax, ay]
    return state

def hitboxes(state):
    boxes = {}
    for key in state["keys"]:
        n, x, y = state["keys"][key]
        if n >= 12:
            boxes[key] = hitbox(x=round(x * 0.6, 2), y=round(y * 0.6, 2))
    return boxes
Adaptive Hitbox: узнаёт, куда на самом деле попадают по каждой клавише, и сдвигает её цель часть пути туда.

suggestions(word, state) ставит слова в начало панели, пока набирается слово. correct(word, fix, state) вызывается один раз на слово, когда его завершает пробел, с исправлением клавиатуры или None, даже при выключенной автокоррекции. Верните слово, чтобы вставить его, False, чтобы оставить слово как набрано, или None, чтобы решила клавиатура. Побеждает первый ответивший плагин, а удаление сразу после отменяет замену, как любую автокоррекцию.

SHORT = {"brb": "be right back", "omw": "on my way", "idk": "I don't know"}

def suggestions(word, state):
    long = SHORT.get(word.lower())
    return [long] if long else []

def correct(word, fix, state):
    if len(word) > 1 and word.isupper():
        return False
    return None
Shorthand: предлагает «be right back», пока набирается brb, и не пускает автокоррекцию к словам заглавными буквами.

Ни один из этих хуков не вызывается в полях пароля. Подсказка исправления на панели показывает исправление клавиатуры, а не то, что вернул бы correct, и таблица, изменённая в on_touch, доходит до клавиатуры на следующем такте.

Проверка в редакторе

Предпросмотр в редакторе это клавиатура-заместитель. Он рисует settings(state) вживую, по нажатию запускает любой хук с образцовым словом, клавишей или типом поля, показывает пробел таким, каким его оставил плагин, и перечисляет всё, что плагин запросил у клавиатуры. stats() там возвращает выдуманные числа, так что скорость появляется без набора.

  1. Используйте элементы в предпросмотре. Каждый запускает on_action и перерисовывает.
  2. Нажимайте кнопки хуков в том порядке, в каком это делала бы клавиатура: on_open, затем несколько раз on_word или on_key, затем on_close.
  3. Читайте консоль. Команды показаны так, как вы их написали, строки print() под ними, а ошибка называет свою строку.
  4. Перезагрузите, чтобы начать состояние заново. Правка скрипта сама по себе его не сбрасывает.

В предпросмотре нет документа, поэтому insert() и replace() только пишут в журнал. Чтобы их проверить, сохраните и печатайте в любом поле с открытой клавиатурой.

Ограничения

Когда плагин приходит файлом или из репозитория, Clink проверяет его перед сохранением. Он должен быть меньше 64 000 байт и 1 600 строк, определять хотя бы один хук, импортировать только разрешённые модули и не содержать ничего из этого списка:

Запрещено в общих плагинах

  • __
  • exec(
  • eval(
  • open(
  • compile(

Модули, которые может импортировать общий плагин

  • json
  • math
  • random
  • re
  • sys
  • time

Каждый вызов хука получает 2 000 000 шагов интерпретатора, столько же, сколько отрисовка панели. Но on_key выполняется при каждом нажатии, поэтому тяжёлая работа в нём замедлит набор задолго до того, как упрётся в этот лимит.

Файл

Плагин это один JSON-документ. id не меняется между обновлениями, version это свободный текст, показанный в списке, icon это имя SF Symbol, а source это скрипт с экранированными переносами строк. Поделитесь им из инспектора редактора или импортируйте кнопкой со стрелкой на вкладке «Плагины».

{
  "id": "word-count",
  "name": "Word count",
  "icon": "text.word.spacing",
  "summary": "Counts words on the space bar",
  "version": "1.0",
  "author": "You",
  "enabled": true,
  "source": "def initial():\n    return {\"on\": False}\n..."
}
Файл .clinkplugin, сокращённый.

Плагины распространяются как файлы .clinkplugin: это JSON-документ со скриптом внутри. Они публикуются через репозиторий так же, как панели, с папкой файлов, манифестом и выпуском. Официальный репозиторий: anti-ltd/clink-plugins.

Репозитории ›

Когда что-то не происходит

Чаще всего дело в одном из этого.

СимптомЧто проверить
Ничего не происходитПлагины выключены вверху вкладки «Плагины», плагин выключен в списке или нет подписки Clink Pro. В любом из трёх случаев клавиатура не запускает ни одного плагина.
Текст пробела затемнёнЕго занял плагин. Строка под полем говорит, какой именно; выключите переключатель этого плагина или сам плагин, чтобы вернуть поле.
Раздел не появляетсяЯкорь написан с ошибкой. Сравните его с «Показывать идентификаторы» в приложении и помните, что раздел всё равно показывается на странице самого плагина, куда стоит заглянуть первым делом.
Клавиша с элементом пустаяПлагин за ней выключен, удалён, или его draw(id, state) выдал ошибку. Раскладка в любом случае сохраняет клавишу, и она снова заполнится, когда плагин включат. Ошибку видно в консоли редактора.
Элемент никогда не меняетсяdraw по-прежнему вызывается, значит не меняется состояние за ним. То, что питает вид, нужно обновлять из хука: on_tick для того, что меняется само, on_word или on_key для того, что следует за набором.
set_setting() ничего не сделалИмя, которого нет в списке выше, вызывает KeyError. Значение не того вида или вне диапазона пропускается, а консоль редактора показывает, что ожидает настройка. Закрытая настройка без подписки ещё и возвращается назад.
Счётчик сбросилсяКлавиатура сохраняет состояние при закрытии, а не на каждой клавише. Клавиатура, которую система завершила посреди сессии, теряет всё, что её плагины насчитали с момента открытия. Держите итоги в хуках со стороны приложения, когда это важно.
Набор кажется медленнымВ on_key выполняется что-то тяжёлое. Перенесите это в on_word, делайте меньше или кэшируйте вычисленное в состоянии.
Эффекта нет в разделе «Из плагинов»Плагины должны быть включены, сам плагин активен, а effects(state) должен возвращать light_effect хотя бы с одним слоем. Нажмите effects в редакторе, чтобы увидеть, что вернулось, и поищите ValueError в консоли.
Элемент верхней панели ничего не делаетИ нажатие, и отпущенный регулятор приходят в on_action(action, value, state), сопоставляясь по id элемента, а не по его имени. Регулятор записывает настройку только тогда, когда bar_knob назвал её в setting=; иначе значение остаётся на плагине, а кнопка настройку не записывает никогда.
PyMini ›
Загрузить в App Store