Автоматизации
Необязательный объект 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Для плагинов нужен Clink Pro. Перед установкой плагина из репозитория Clink попросит разрешить код из этого репозитория, так же как для панелей и действий.
on_key и on_word получают то, что вы набираете: так работает, например, счётчик слов. У PyMini нет доступа к сети и файлам, поэтому плагин не может отправить набранный текст через интернет, а то, что он сохраняет, остаётся в Clink на вашем устройстве.
Ваш первый плагин
Быстрее всего начать со стартового скрипта, который приложение пишет за вас. Он ставит переключатель в «Клавиши» > «Пробел» и, пока тот включён, считает слова на пробеле. Пять шагов, ничего скачивать не нужно.
- Откройте вкладку «Плагины», включите плагины сверху и нажмите + для нового плагина.
- Редактор откроется на стартовом скрипте. Прочитайте его один раз: initial(), settings(), on_action(), on_open() и on_word() это всё.
- Переключитесь на предпросмотр. Включите переключатель, затем несколько раз нажмите on_open и on_word и следите за макетом пробела и консолью.
- Сохраните. Плагин включён в списке, а его переключатель теперь есть и в «Клавиши» > «Пробел».
- Откройте клавиатуру где угодно и печатайте. Пробел считает вместе с вами.
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Возвращать 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.heatmapanalytics.privacyanalytics.trendsanalytics.typing-testautomations.rulesgestures.accentsgestures.cursorgestures.deletegestures.generalgestures.suggestionsgestures.swipehaptics.feelkeys.adaptivekeys.faceskeys.hitboxeskeys.hitmapkeys.long-presskeys.numberrowkeys.onehandedkeys.roundnesskeys.sizekeys.spacebarkeys.spacingkeys.splitlanguages.applanguages.customlanguages.managelanguages.packslanguages.switchlanguages.typinglayout.arrangelayout.arrangementlayout.buildlayout.longpresslayout.presetslayout.topbarmotion.deletemotion.entrancemotion.glowmotion.key-pressmotion.key-responsemotion.lettersmotion.space-responsemotion.transitionpopups.stylesound.keysoundstext.automationtext.contenttext.correctionstext.historytext.punctuationtext.speedtext.suggestionstext.symbolsthemes.backgroundthemes.canvasthemes.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()
analyticsemoji.skin_toneemoji.trailing_spacegestures.cursorgestures.cursor_stylegestures.highlight_shiftgestures.plane_slidegestures.predictive_flickgestures.quick_accentgestures.swipegestures.swipe_deletegestures.swipe_multi_wordgestures.swipe_space_commitgestures.swipe_two_thumbgestures.trailgestures.trail_stylegestures.trail_widthhaptics.enabledhaptics.intensityhaptics.sharpnesskeys.accentskeys.glyph_scalekeys.heightkeys.long_press_hintkeys.popup_stylekeys.popupskeys.radiuskeys.row_spacingkeys.spacingkeys.uppercasekeys.widthlanguagelanguage.bar_keylanguage.bar_key_stylelanguage.modelayoutlayout.dismiss_shortcutlayout.number_rowlayout.number_row_scalelayout.one_handedlayout.one_handed_shortcutlayout.one_handed_sidelayout.one_handed_widthlayout.splitlayout.split_gaplayout.split_number_rowlayout.split_shortcutlayout.split_space_barpet.cornerpet.enabledpet.speciessettings.accentHoldDelaysettings.accentMoveCancelsettings.activateWithIconsettings.adaptiveGrowsettings.adaptiveHitboxessettings.adaptivePredictAtWordStartsettings.adaptivePredictionWeightsettings.adaptiveShrinksettings.adaptiveSpacesettings.aiAutocorrectsettings.aiCompletionssettings.aiExtensionEnabledsettings.aiSearchsettings.aiToolsColorOverridessettings.aiToolsDiffStylesettings.aiToolsDisabledsettings.aiToolsLayoutStylesettings.aiToolsOrdersettings.aiToolsPromptOverridessettings.aiTranslatesettings.alternatingSplitsettings.arabicIndicNumeralssettings.backgroundEffectOverridesettings.clipboardCloseOnPastesettings.clipboardDeleteOnPastesettings.clipboardIgnoreImagessettings.clipboardIgnorePinsOnDeletesettings.clipboardStylesettings.cursorActivationHapticsettings.cursorLineStridesettings.cursorStepHapticsettings.customPanelsStandalonesettings.customPetIDsettings.deleteWordSwipeEngagesettings.deleteWordSwipeStridesettings.dictationAssistsettings.dictationAssistCustomsettings.dictationAssistLevelsettings.dictationColorSchemesettings.dictationGlassCapsulesettings.dictationSmartActionssettings.dictationStylesettings.dictationVisualStylesettings.dragUpThresholdsettings.emojiCategoryOrdersettings.emojiCellSpacingsettings.emojiColumnCountsettings.emojiCrossAxisSwitchesTabsettings.emojiCustomSetssettings.emojiGlyphScalesettings.emojiHiddenCategoriessettings.emojiHiddenFromPanelssettings.emojiRecentsCapsettings.emojiRecentsSortsettings.emojiRememberCategorysettings.emojiRowCountsettings.emojiScrollDirectionsettings.emojiSearchSlotsettings.emojiShowABCKeysettings.emojiShowBackspaceKeysettings.emojiStartCategoryIDsettings.emojiTabBarSlotsettings.emojiTabIconStylesettings.emojiToneHoldDelaysettings.extensionOrdersettings.extraTopBarssettings.formFreeCornerssettings.formLayoutEnabledsettings.gifShareAsLinksettings.glassPerRowMergesettings.glassReleaseResponsesettings.gridSwitchAnimationsettings.gridSwitchDurationsettings.handwritingInkColorsettings.handwritingInkGlowsettings.handwritingInkStylesettings.handwritingInkWidthsettings.hitboxScalesettings.iconPickerStylesettings.keyBloomScalesettings.keyLightingsettings.keyPressGlowsettings.keyPressInstantsettings.keyPressLingersettings.keySpringDampingsettings.keySpringResponsesettings.keyboardBottomPaddingsettings.keyboardLanguagessettings.keyboardTopPaddingsettings.longPressGlyphScalesettings.minPressVisiblesettings.notepadBrowseStylesettings.notepadModesettings.numberRowFontSizesettings.numberRowLeadingKeyssettings.numberRowTrailingKeyssettings.oneHandedCustomKeyssettings.oneHandedExtraKeyssettings.panelButtonHitboxScalesettings.persistentLeadingKeyssettings.persistentTrailingKeyssettings.petIdleMotionsettings.petScalesettings.pinyinFuzzyEnabledsettings.pluginLookssettings.popupSpringDampingsettings.popupSpringResponsesettings.predictiveFlickSuggestionPositionsettings.predictiveFlickSuggestionsSeparatesettings.reduceEffectsOnLowPowersettings.repeatAccelStepsettings.repeatHoldDelaysettings.repeatInitialIntervalsettings.repeatMinIntervalsettings.replacementsLayoutsettings.rowInsetssettings.secondaryNeuralModelsEnabledsettings.separateActivationsettings.separateLanguageLayoutssettings.showCaptureOverlaysettings.showCorrectionFieldsettings.showHitboxOverlaysettings.showIconsBeforeTypingsettings.showRecentEmojisettings.showTouchHeatmapsettings.showTouchSurfaceBoundssettings.showTouchTelemetryOverlaysettings.slideUpPickerStylesettings.solidPopupOpacitysettings.soundVoicesettings.spaceBloomScalesettings.spaceCursorActivationDelaysettings.spaceCursorDragScalesettings.spaceCursorStridesettings.spaceLeanMultipliersettings.spaceSpringDampingsettings.spaceSpringResponsesettings.spatialBiasEnabledsettings.spatialBiasGainsettings.splitIndicessettings.splitOtherPlanessettings.stickerGridSnapsettings.stickerPlacementssettings.suggestionDebounceDelaysettings.suggestionHitboxScalesettings.suggestionSegmentLiquidGlasssettings.suggestionSegmentStylesettings.suggestionSegmentsFollowThemesettings.suggestionSeparatorColorsettings.suggestionSeparatorStylesettings.suggestionTopPaddingsettings.swipeKeyMorphsettings.swipeMorphRadiussettings.swipeMorphStrengthsettings.swipeTrailEndWidthsettings.swipeTrailMaxLengthsettings.swipeTrailStartWidthsettings.swipeTrailTrimsettings.toolsButtonStylesettings.toolsHiddenFromPanelssettings.topBarItemssettings.topBarOrdersettings.translateLanguageOrdersettings.translateLanguagesDisabledsettings.translateStylesettings.translateTonesettings.vietnameseInputMethodsound.enabledsound.packsound.volumespacebar.cornerspacebar.language_codespacebar.sizespacebar.textstickers.enabledtext.arithmetictext.auto_capitalizetext.auto_punctuationtext.autocorrecttext.autocorrect_everywheretext.contactstext.conversionstext.double_space_periodtext.learningtext.punctuation_spacingtext.return_to_letterstext.revert_on_deletetext.smart_quotestext.suggestionstext.suggestions_animationtext.suggestions_heighttext.suggestions_scrolltext.suggestions_stylethemetheme.backgroundtheme.darktheme.delete_glyphtheme.entrancetheme.glyph_presstheme.lighttheme.match_systemtheme.press_styletheme.reactive_backgroundtools.calculatortools.clingtools.clipboardtools.conversiontools.dictationtools.dictionarytools.emojitools.giftools.handwritingtools.layout_switchertools.notepadtools.plugin_switchertools.profilestools.replacementstools.textfxtools.theme_switchertools.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 stateSHORTCUTS = {"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 statedef 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 statedef 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 stateNAMES = ["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 statedef 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Элементы в раскладке
Пользовательская раскладка собирается из клавиш и элементов: полоса недавних эмодзи, ряд цифр, курсорная площадка. Плагин может добавить свои. 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")Вид элемента это дерево узлов, как у страницы настроек, но клавиша не страница настроек: используются только рисующие узлы, а всё нажимаемое игнорируется. Клавиша уже принадлежит раскладке, поэтому внутри неё нет места для кнопки.
ЧТО МОЖЕТ РИСОВАТЬ ЭЛЕМЕНТ
sparklinetextbadgeiconprogresshstackvstackspacerdivider
Рассчитывайте на клавишу. 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)Элементы живут в пользовательских раскладках, так что сперва нужно собрать раскладку. Готовые её не удержат: скопировать готовую раскладку в свою это первое, что предложит редактор.
- Установите плагин и включите его. Его элементы появятся сразу же.
- Откройте «Раскладка», создайте или скопируйте свою раскладку и перейдите в «Расположение».
- Нажмите «Элемент», выберите элемент плагина в полосе и задайте ширину.
Предпросмотр в редакторе рисует каждый элемент, который предлагает скрипт, примерно в размер клавиши, где он окажется, под макетом пробела. Сохраните, разместите один раз, и клавиатура нарисует то же самое.
Элемент показывает только то, что рисует плагин: нажатие на него ничего не делает, потому что клавиша уже принадлежит раскладке. Раскладка хранит элемент, даже когда плагин выключен или удалён, и клавиша снова заполняется, когда он возвращается.
Кнопки и регуляторы в верхней панели
Полоса над клавишами собирается в разделе «Раскладка» > «Верхняя панель», и плагин может предложить для неё свои элементы управления. 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". Если опустить оба, регулятор будет кольцом со значком внутри; в редакторе уже стоящему в панели регулятору можно к тому же выбрать любую встроенную отделку.
Само по себе в панель ничего не попадает. Элемент ставят вручную, как меню и подсказки.
- Установите плагин и включите его. Его элементы панели появятся сразу.
- Откройте «Раскладка» > «Верхняя панель».
- Добавьте кнопку или регулятор плагина и перетащите на нужное место. Для регулятора можно ещё выбрать отделку.
Конструкторы молча пропускают незнакомое ключевое слово, без ошибки. 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"),
]),
]Узоры
solidpulsewavegradienttwinklesweeprainflickerchecker
| Ключ | Что делает |
|---|---|
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),
]),
]Выбранный эффект копируется в настройки пользователя, поэтому работает и с выключенным плагином. Пока плагин включён и один из его эффектов активен, клавиатура перечитывает 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),
]),
]Кнопка 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),
]),
]Возвращайте узлы 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)}Фигуры
circlerectcapsulelinepathtexticon
Шестнадцать фигур на клавишу, считая все её слои, и 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),
}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 boxessuggestions(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Ни один из этих хуков не вызывается в полях пароля. Подсказка исправления на панели показывает исправление клавиатуры, а не то, что вернул бы correct, и таблица, изменённая в on_touch, доходит до клавиатуры на следующем такте.
Проверка в редакторе
Предпросмотр в редакторе это клавиатура-заместитель. Он рисует settings(state) вживую, по нажатию запускает любой хук с образцовым словом, клавишей или типом поля, показывает пробел таким, каким его оставил плагин, и перечисляет всё, что плагин запросил у клавиатуры. stats() там возвращает выдуманные числа, так что скорость появляется без набора.
- Используйте элементы в предпросмотре. Каждый запускает on_action и перерисовывает.
- Нажимайте кнопки хуков в том порядке, в каком это делала бы клавиатура: on_open, затем несколько раз on_word или on_key, затем on_close.
- Читайте консоль. Команды показаны так, как вы их написали, строки print() под ними, а ошибка называет свою строку.
- Перезагрузите, чтобы начать состояние заново. Правка скрипта сама по себе его не сбрасывает.
В предпросмотре нет документа, поэтому insert() и replace() только пишут в журнал. Чтобы их проверить, сохраните и печатайте в любом поле с открытой клавиатурой.
Ограничения
Когда плагин приходит файлом или из репозитория, Clink проверяет его перед сохранением. Он должен быть меньше 64 000 байт и 1 600 строк, определять хотя бы один хук, импортировать только разрешённые модули и не содержать ничего из этого списка:
Запрещено в общих плагинах
__exec(eval(open(compile(
Модули, которые может импортировать общий плагин
jsonmathrandomresystime
Каждый вызов хука получает 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: это 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=; иначе значение остаётся на плагине, а кнопка настройку не записывает никогда. |