自动化
.clinkplugin 文件中的可选 automation 对象声明事件、状态、设置、命令、预设和快捷槽位。每项功能都有稳定的 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) | 插件的某个控件被使用时调用。不带 value 的 on_action(action, state) 也可以。 |
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) | 动态背景。返回由 particles(...) 图层构成的 background(...) 条目。 |
layouts(state) | 键盘布局。返回 layout(...) 条目。 |
trails(state) | 这个插件提供给拖尾选择器的滑行拖尾。返回 trail(...);见下面的外观一节。 |
haptics(state) | 每个按键的触感反馈,是一个从按键名到触感的 dict。键盘打开时读取一次,之后大约每秒读取一次。 |
hitboxes(state) | 每个按键的点击区域,是一个从按键名到 hitbox(...) 的 dict。与 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 写入状态,要么为 on_action 指定一个 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 指定回车要运行的处理函数名。 |
button(label, action="", value=None, insert="", set=None, style="plain", icon="", enabled=True) | 一个按钮。insert 会把它的文字打进你正在写的地方,set 把键合并进 state,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 自身设置界面上某张卡片的 ID。在 section() 中指定它,插件的控件就会绘制在那张卡片正下方,上方标着插件名。最简单的查找方法:在应用里打开“更多 > 开发者”,打开“显示 ID”。每张卡片都会出现一个小小的信息标记,显示其 ID,点按即可复制;页面的 ID 显示在标题栏。
分区可以指定的所有锚点
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 中接管,在关闭功能的 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 这样的语言 id 从内置的旗帜图形里取。None 恢复原生行为。 |
space_language_emoji(language) | 同一个标记,改用该地区的旗帜 emoji。三个标记命令共用一个位置,所以打开一个标记插件就会关掉其他的。 |
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() | 返回一个包含 wpm、peak_wpm、keystrokes、words 和 streak 的 dict。插件开启时 wpm 实时更新。总数来自“统计”,如果关闭了统计,总数就不再更新。 |
setting(name) | 按名称读取下面列出的一项设置。其他名称会引发 KeyError。 |
context() | 和面板读到的同一份快照,插件还多出 suggestion、shift(off、on 或 locked)和 plane。文档相关的几个键要靠打字数据权限才会有内容;shift 没有权限也能读。 |
| 键 | 包含内容 |
|---|---|
stats()["wpm"] | 最近几秒的每分钟字数,按真正进入输入框的字符计算。开始输入一两秒后出现,停顿时下降,停下来就归零。删除永远不会让它增加。 |
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) 收到其中一个 id,返回要绘制的内容。一个插件可以提供多个,draw 会按 id 逐个被调用。
| 调用 | 作用 |
|---|---|
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= 则把它绑到 app 自己的数值设置上,范围和当前位置都由那里提供。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= 的旋钮转的是 app 已有的值,名字取自上面“命令与读取”一节的列表,比如 sound.volume 或 haptics.intensity。app 不认识的名字会在控制台里报 KeyError。范围和起始位置都来自那个设置;拖动时键盘自己的副本就已经改了,所以下一次敲键就是新的音量,值则在松手时保存。音量或触感旋钮从零往上拧时,拖动期间还会把声音或触感重新打开,这样一路上的档位听得见也摸得着;保存这个开关要靠插件自己在 on_action 里调用 set_setting。
旋钮可以用 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,没有图层的效果会被略过。
外观
有七个钩子为 App 提供可选的东西: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),
]),
]从 trails(state) 返回 trail 节点。每条轨迹都有 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)])]外观在被选中时就已复制,所以之后修改脚本不会改变别人已在使用的外观,需要重新选择。拼错的关键字、不在列表中的词,或本该是数字却写成文字,都会在编辑器控制台中报错;数值会被限制在 App 自身控件使用的范围内。
画在键上的图形
key_art(state) 返回一个从按键名到图形的 dict,键盘会把它画在键面里。按键名和 haptics 用的一样,letters 和 keys 兜底也照样有效。图形可以是 shape(...) 列表、单个形状、一层 art([...]),或者一组层;每一层按自己的节奏过渡到下一幅画,所以一个键上可以放两样速度不同的东西。
| 调用 | 作用 |
|---|---|
art(shapes, animate=0, curve="ease_out") | 一层会动的图。给它一幅新画,它就会用 animate 秒沿着 curve 过渡过去,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。它从键上的 anchor 起按 x 和 y 定位,单位是点,或者用 unit="key" 时按键的比例。fill 和 stroke 接受十六进制颜色、"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) | 一种涂色:十六进制颜色,或者 "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 之外的全部事件;这五个每次按键或每秒都要跑一遍脚本,所以必须点名索取。shift 和平面的变化无论有没有人听都会重画图形,也会送进 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 会员。三种情况下键盘都不会运行任何插件。 |
| 空格键文字显示为灰色 | 某个插件接管了它。输入框下方那行会标明是哪个;关闭该插件的开关或插件本身,即可取回输入框。 |
| 分区没有出现 | 锚点拼写有误。与应用中的“显示 ID”对照,并记住分区仍会显示在插件自己的页面上,先去那里看。 |
| 元素键是空白的 | 它背后的插件被关闭、被卸载,或者它的 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= 里指名了某个设置,旋钮才会写设置;否则这个值要由插件自己处理,而按钮根本不会写设置。 |