自动化

.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
WPM Spacebar 会在“按键 > 空格键”下添加一个开关。打开后,空格键上会显示你当前的打字速度。

插件需要 Clink Pro。从仓库安装插件前,Clink 会请你允许运行该仓库的代码,和面板、动作一样。

on_key 和 on_word 会收到你输入的内容,计词器就是靠这个工作的。PyMini 无法访问网络或文件,所以插件无法通过互联网发送你输入的内容;插件保存的内容由 Clink 保存在你的设备上。

你的第一个插件

最快的入门方式是应用替你写好的起始脚本。它在“按键 > 空格键”下放一个开关,开关打开时在空格键上计数单词。五步完成,无需下载任何文件。

  1. 打开插件标签页,在顶部打开插件功能,然后点按 + 新建插件。
  2. 编辑器打开时就是起始脚本。通读一遍:initial()、settings()、on_action()、on_open() 和 on_word() 就是全部。
  3. 切换到预览。打开开关,然后点几次 on_open 和 on_word,观察空格键模型和控制台。
  4. 保存。插件在列表中已开启,它的开关现在也出现在“按键 > 空格键”下。
  5. 在任意地方打开键盘并输入。空格键会一路计数。
def initial():
    return {"on": False, "words": 0}

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

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

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

def on_word(word, state):
    state["words"] += 1
    if state["on"]:
        space_text(f"{state['words']} words")
    return state
应用写出的起始脚本。

钩子

这 33 个函数可以任选定义,用不到的省略即可。每个函数都把 state 作为最后一个参数接收。对某件事做出反应的函数会把 state 返回,无论是否修改过。回答某个问题的函数(elements、draw、effects、外观钩子、haptics、hitboxes、suggestions 和 correct)返回它们的答案,同时仍可以直接修改 state。

钩子调用时机
on_automation(command, args, state)on_automation(command, args, state) 在现有沙盒中运行获准的本地命令。它可以更新空格键文字、发布已声明的状态或发出已声明的事件。此钩子不允许插入文本、访问剪贴板、永久更改设置或发出外部请求。
initial()在还没有任何已保存状态时调用一次。返回一个 dict,内容可以是任何 JSON 能保存的值。
settings(state)在应用中显示插件设置时调用。以节点树形式返回插件的控件。它不会在键盘中运行。
on_action(action, value, state)插件的某个控件被使用时调用。不带 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
每次会话由 on_open 重置的计数,以及一直保留的总数。

返回 state 是值得保持的习惯,但并非硬性要求:dict 是引用,原地修改同样有效。返回另一个 dict,比如上面的 initial(),它就会成为新的状态。

设置与分区

settings(state) 返回用面板构建器组成的节点树:text、toggle、slider、stepper、segmented、button、row、field 以及各种布局。它显示在插件标签页中该插件的页面上。把其中一部分包进 section(anchor, children, title),这部分还会出现在 Clink 自己的某个设置界面上,紧挨着相关的设置。

def settings(state):
    return vstack([
        text("Counts words as you type.", size=13),
        section("keys.spacebar", [
            toggle("Count on the space bar", state["on"], action="toggle"),
            stepper(state["goal"], min=10, max=500, step=10, label="Goal", key="goal"),
        ], title="Word count"),
    ])
开关和步进器会出现在“按键 > 空格键”下。说明文字只出现在插件自己的页面上。

插件最常用的控件。每个控件要么把新值以 key 写入状态,要么为 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.heatmap
  • analytics.privacy
  • analytics.trends
  • analytics.typing-test
  • automations.rules
  • gestures.accents
  • gestures.cursor
  • gestures.delete
  • gestures.general
  • gestures.suggestions
  • gestures.swipe
  • haptics.feel
  • keys.adaptive
  • keys.faces
  • keys.hitboxes
  • keys.hitmap
  • keys.long-press
  • keys.numberrow
  • keys.onehanded
  • keys.roundness
  • keys.size
  • keys.spacebar
  • keys.spacing
  • keys.split
  • languages.app
  • languages.custom
  • languages.manage
  • languages.packs
  • languages.switch
  • languages.typing
  • layout.arrange
  • layout.arrangement
  • layout.build
  • layout.longpress
  • layout.presets
  • layout.topbar
  • motion.delete
  • motion.entrance
  • motion.glow
  • motion.key-press
  • motion.key-response
  • motion.letters
  • motion.space-response
  • motion.transition
  • popups.style
  • sound.keysounds
  • text.automation
  • text.content
  • text.corrections
  • text.history
  • text.punctuation
  • text.speed
  • text.suggestions
  • text.symbols
  • themes.background
  • themes.canvas
  • themes.theme

keys.spacebar 在空格键界面上有专属位置。锚点不在此列表中的分区仍会显示在插件页面上,所以打错字只会失去一张卡片,不会失去插件。

接管

当插件驱动 Clink 自身的某项设置时,用户应当能看到,两者也不该互相争抢。claim(control) 宣告插件拥有它:应用会在该设置的卡片上标出插件名,占用期间空格键文字栏会被锁定。release(control) 交还它。在打开功能的 on_action 中接管,在关闭功能的 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() 接受的名称

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

命令

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

和面板的 view() 一样,settings() 只应描述控件。在其中调用的命令会被忽略,并记录到控制台。

示例

五个小插件,每个都完整。粘贴到新插件里,保存,就能运行。

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

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

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

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

def on_language(code, state):
    if state["on"]:
        space_text(code.upper())
    return state
语言徽章:空格键上显示输入语言,切换时立即更新。
SHORTCUTS = {"omw": "on my way", "brb": "be right back", "ty": "thank you"}

def on_key(key, state):
    if key != " ":
        return state
    words = context()["before"].split()
    if words and words[-1] in SHORTCUTS:
        # the space is already in the text, so it goes too and comes back after
        replace(len(words[-1]) + 1, SHORTCUTS[words[-1]] + " ")
    return state
快捷短语:输入 omw 加一个空格,得到 on my way。on_key 在空格已输入后才看到它,所以替换会连空格一起处理。
def initial():
    return {"goal": 200, "count": 0}

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

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

def on_word(word, state):
    state["count"] += 1
    if state["count"] == state["goal"]:
        haptic("medium")
        banner("Goal reached")
    return state
单词目标:插件页面上的步进器、一个进度条,当本次会话达到目标时震动并显示横幅。
def initial():
    return {"muted": False, "was_on": True}

def on_field(kind, state):
    # a password or a number field is not a place for key sounds
    quiet = kind in ("password", "number", "phone")
    if quiet and not state["muted"]:
        state["was_on"] = setting("sound.enabled")
        state["muted"] = True
        set_setting("sound.enabled", False)
    elif not quiet and state["muted"]:
        state["muted"] = False
        if state["was_on"]:
            set_setting("sound.enabled", True)
    return state
安静输入框:在密码或数字输入框中关闭按键声,之后恢复,用到 on_field、setting() 和 set_setting()。
NAMES = ["sam", "ana", "team"]

def on_key(key, state):
    typing = context()["before"].split(" ")[-1]
    if typing.startswith("@"):
        start = typing[1:].lower()
        suggest([n for n in NAMES if n.startswith(start)])
        state["showing"] = True
    elif state.get("showing"):
        suggest([])                 # the mention is over, give the bar back
        state["showing"] = False
    return state
提及:输入 @ 时,你自己的名字会出现在候选栏。
def on_swipe(direction, state):
    if direction == "left":
        delete_word()
    elif direction == "right":
        press("space")              # the space bar's own path, so it autocorrects
    elif direction == "up":
        pick_suggestion("center")
    elif direction == "up_left":
        pick_suggestion("left")
    elif direction == "up_right":
        pick_suggestion("right")
    return state
Fleksy 式的划动手势,也是 Flick Gestures 插件的核心:向左删除一个词,向右输入空格,向上选取候选。只有在滑动输入关闭时才会收到划动。

布局中的元素

自定义布局由按键和元素组成:最近表情的横条、一排数字、光标触控板。插件可以添加自己的元素。elements(state) 说明它提供什么,draw(id, state) 负责绘制,于是一个键可以显示插件能算出的任何东西:打字速度曲线、时钟、倒计时,或者你自己的电量条。

两个钩子共同构成一个元素。elements(state) 列出你提供的东西,每个元素一条,应用读取它来填充布局编辑器的选择条。draw(id, state) 收到其中一个 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")
完整的 WPM Sparkline 插件:它提供一个元素,并在上面画出最近三十秒的速度。

元素的画面是一棵节点树,和设置页一样,但按键不是设置页:只有负责绘制的节点会被使用,可点按的一概忽略。这个键已经属于布局,里面没有地方再放一个按钮。

元素可以绘制的内容

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

按一个键的尺寸来设计。width 以键宽为单位:1 是一个字母键,3 大约是一行的三分之一,之后使用者还可以调整。文字只有一行,会缩小以放得下;颜色默认跟随键的文字颜色,所以不特别指定时元素会和周围的键相配。sparkline 保留最近 120 个点,比一个键能显示的还多。

import time

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

def draw(id, state):
    return text(time.format("HH:mm"), size=15, weight="semibold")
键上的时钟:没有状态,没有钩子,每次被问就给出当前时间。元素大约每秒会自行重绘一次。
GOAL = 200

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

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

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

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

def draw(id, state):
    return vstack([
        text(f"{state['words']}/{GOAL}", size=11),
        progress(state["words"], total=GOAL),
    ], spacing=2)
一次输入会话的字数目标,由 on_word 计数,画成一条进度条。状态由钩子维护,draw 只负责读取。

元素只存在于自定义布局中,所以得先有一个布局。预设布局放不下元素:把预设复刻成你自己的布局,正是编辑器首先会提示的事。

  1. 安装插件并打开它。它的元素会立刻出现。
  2. 打开布局,新建或复刻一个自定义布局,然后进入排列。
  3. 点按元素,从横条中选择插件的元素,再设置它的宽度。

编辑器的预览会把脚本提供的每个元素都画出来,大小接近它将来所在的按键,位置在空格键示意图下方。保存并放置一次,键盘上画的就是同一个东西。

元素只展示插件画出的内容:点按它不会有反应,因为这个键已经属于布局。即使插件被关闭或卸载,布局仍会保留这个元素,插件回来后键就会重新填满。

顶栏里的按钮和旋钮

按键上方的那条栏在布局 > 顶栏里编排,插件可以往里放自己的控件。bar_items(state) 返回它们:bar_button(...) 用来点按,bar_knob(...) 用来旋转。这个列表在键盘打开时读取,每次点按后再读一次,所以一个项目可以随插件状态出现和消失。

调用作用
bar_items(state)这个插件提供给顶栏的全部内容,以列表返回。id 只需在插件内部唯一,顶栏会把它和插件自己的 id 一起记下。id 重复时保留第一个,钩子不再返回的项目就不再绘制。
bar_button(id, name, icon="", title="")一个按钮。name 是编辑器里列出的名字,也是 VoiceOver 读出来的内容。icon 是 SF Symbol,title 是旁边最多 12 个字符的文字:有图标而没有 title 时只画图标,没有图标时只画文字。按钮没有值,也没有范围。点按会调用 on_action(id, None, state),按钮要做的事全都在那里发生。
bar_knob(id, name, icon="", min=0, max=1, step=0, value=0, setting=None, art=None, rotor=None)一个旋钮。min 和 max 是它的范围,step 大于零时会按档位吸附,value 是起始位置。改用 setting= 则把它绑到 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"。两个都不给,旋钮就是一个中间带图标的圆环;已经放进顶栏的旋钮也可以在编辑器里换成任意一种内置质感。

没有东西会自己跑到顶栏里。项目要手动放进去,和菜单、候选词一样。

  1. 安装插件并打开它。它的顶栏项目会立刻出现。
  2. 打开布局 > 顶栏。
  3. 添加插件的按钮或旋钮,拖到该放的位置。旋钮还可以挑一种质感。

构造函数遇到不认识的关键字会直接跳过,不报错。setting=、min=、max=、step=、value=、art= 和 rotor= 只属于 bar_knob,所以给 bar_button 传了其中任何一个,它照样只是一个普通按钮,也不会有任何提示:按钮该做的事写在 on_action 里。反过来 title= 属于按钮,旋钮会跳过它。

灯光效果

插件可以添加自己的按键灯光。effects(state) 返回 light_effect(...) 条目,每一个都会出现在应用的“效果 > 来自插件”下,与内置样式和大家自己做的效果并列。效果是数据而不是绘制:一叠由键盘自己驱动动画的图层。因此脚本不会逐帧运行,按键表面、字母、光晕、按键时闪亮和空闲休眠都与内置样式一样正常工作。

调用作用
light_effect(id, name, layers=[...], colors=[], icon="")一个效果。id 只需在插件内唯一。layers 是 light_layer(...) 的列表,从上往下应用。colors 是颜色循环经过的最多八个 "#rrggbb" 字符串;省略时,效果跟随用户的“颜色”设置。
light_layer(pattern, ...)一个图层:从下面列表中选一种图案。所有关键字都是可选的。
COLORS = ["#00e5ff", "#7c4dff", "#ff4081"]

def effects(state):
    return [
        light_effect("tide", "Tide", colors=COLORS, layers=[
            light_layer("solid", low=0.2, high=0.2),
            light_layer("wave", moves="both", speed=0.8, size=1.5,
                        direction="up"),
        ]),
    ]
稳定的底色之上,一道缓慢的波浪穿过三种颜色。波浪把光加到底色上,并一路改变颜色。

图案

  • solid
  • pulse
  • wave
  • gradient
  • twinkle
  • sweep
  • rain
  • flicker
  • checker
作用
moves="light"图案改变的内容:"light" 表示亮度,"color" 或 "both"。
mix="add"它的亮度如何与上面的图层结合:"add",保留更亮者的 "max",或像遮罩一样调暗的 "multiply"。
shape="smooth"pulse、wave 和 gradient 的起伏形状:"smooth"、"ramp"、"step" 或 "spike"。
direction="right"wave、gradient、sweep 和 rain 的移动方向:"right"、"left"、"down"、"up",或从中间向外的 "out"。
speed=1从 0 到 4。为 0 时图案静止不动。
size=1从 0.25 到 4:图案在整个键盘上重复的次数。对 twinkle 来说决定亮起的按键数量,对 sweep 来说决定尾巴的长度。
low=0, high=1图案在其间变化的亮度,各自从 0 到 1。low 高于 high 时图案会反转。
color_span=1, color_offset=0图案在颜色中前进多远,以及从哪里开始,各自从 0 到 1。
def effects(state):
    return [
        light_effect("scanner", "Scanner", layers=[
            light_layer("wave", moves="color", speed=0.3),
            light_layer("sweep", mix="multiply", speed=1.2, size=1.5),
        ]),
    ]
把 sweep 当作遮罩。第一个图层只改变颜色,由 sweep 决定哪些按键亮起。没有 colors 时它跟随“颜色”设置,所以选“光谱”就成了彩虹。

选择一个效果会把它复制到用户的设置中,所以即使关闭插件它也会继续工作。只要插件开启且它的某个效果正在运行,键盘就会在打开时以及之后大约每秒重新读取 effects(state),并换上变化的部分。效果就是这样跟随状态、时间或 stats() 的。

def effects(state):
    # Rounded, so the effect only changes when the pace really does.
    tempo = round(0.3 + min(stats()["wpm"], 120) / 40, 1)
    return [
        light_effect("tempo", "Tempo", colors=["#ff3d7f", "#ffb000"], layers=[
            light_layer("wave", moves="both", speed=tempo, low=0.15),
        ]),
    ]
来自 Light Show 插件的 Tempo:打字越快,波浪越快。图层可以改变速度,而图案不会跳动。

编辑器里的 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),
        ]),
    ]
Snowfall:摇曳飘落的雪花,以及每次按键时泛起的一点光。

从 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)}
一盏大写锁定指示灯,由插件画出来,而不是键盘自带的

形状

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

每个键最多十六个形状,按这个键上所有层一起算;每条路径最多 64 个点,超出的会被丢掉。形状画在自己 width 和 height 组成的盒子里,没有厚度的盒子什么也画不出来,所以一条水平线需要给自己一点 height,并让点走在它正中(height=0.03,points=[[0, 0.5], [1, 0.5]]),否则它会一声不响地不出现。

触感反馈、点击区域和更正

haptics(state) 和 hitboxes(state) 返回以按键名为键的 dict:字母本身、"space"、"delete"、"return"、"shift" 或 "globe",未列出的字母用 "letters",其余一切用 "keys"。插件没有列出的键保留用户自己的设置。两张表都在键盘打开时读取一次,之后大约每秒读取一次,所以不会因为它们在每次按键时运行任何东西。

调用作用
feel(style=None, intensity=None, sharpness=None)一种触感反馈:style 为 "soft"、"light"、"medium"、"heavy"、"rigid" 或 "off",intensity 和 sharpness(0 到 1)用来微调。只写一个样式词也可以。
hitbox(scale=1, x=0, y=0)一个点击区域:x 和 y 按按键尺寸的比例移动它的目标,最多半个键;scale 放大或缩小目标。单独一个数字表示 scale。
def haptics(state):
    return {
        "space": "heavy",
        "return": "rigid",
        "delete": feel(intensity=0.45, sharpness=0.9),
    }
Heavy Space:更厚重的空格键、干脆的回车,以及轻柔的删除。

on_touch(key, x, y, state) 在每次点按处理完之后运行,并告诉你手指落在键上的哪个位置。位置是相对于画出来的按键测量的,而不是移动后的目标,所以插件可以把每个键挪向实际被按的地方,而不会追着自己的偏移跑。

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

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

def hitboxes(state):
    boxes = {}
    for key in state["keys"]:
        n, x, y = state["keys"][key]
        if n >= 12:
            boxes[key] = hitbox(x=round(x * 0.6, 2), y=round(y * 0.6, 2))
    return boxes
Adaptive Hitbox:学习每个键实际被按在哪里,并把它的目标朝那里挪动一部分。

suggestions(word, state) 在输入一个词时把词放在建议栏最前面。correct(word, fix, state) 在空格结束一个词时每个词运行一次,传入键盘自己的更正或 None,即使自动更正已关闭也会运行。返回一个词让它上屏,返回 False 保持原样,或返回 None 交给键盘决定。最先给出答案的插件胜出,紧接着按删除键可以像撤销任何自动更正一样撤销它。

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

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

def correct(word, fix, state):
    if len(word) > 1 and word.isupper():
        return False
    return None
Shorthand:输入 brb 时建议“be right back”,并让自动更正不去碰全大写的词。

这两个钩子都不会在密码输入框中运行。建议栏中的更正提示显示的是键盘自己的更正,而不是 correct 会返回的内容;在 on_touch 中修改的表会在下一次读取时传到键盘。

在编辑器中测试

编辑器的预览是一个替身键盘。它实时绘制 settings(state),点一下就能用示例单词、按键或输入框类型触发任意钩子,显示插件留下的空格键状态,并列出插件向键盘提出的所有请求。那里的 stats() 返回虚构数字,所以无需输入也能看到速率。

  1. 使用预览中的控件。每个控件都会运行 on_action 并重绘。
  2. 按键盘的调用顺序点按钩子按钮:先 on_open,再点几次 on_word 或 on_key,最后 on_close。
  3. 查看控制台。命令按你写的样子显示,print() 的输出在下面,错误会标出行号。
  4. 重新加载可让状态从头开始。编辑脚本本身不会重置状态。

预览没有文档,所以 insert() 和 replace() 只会记录日志。要试用它们,请保存后打开键盘在任意输入框中输入。

限制

插件以文件形式或从仓库进来时,Clink 会在保存前检查它。它必须小于 64,000 字节、少于 1,600 行,至少定义一个钩子,只导入允许的模块,并且不含以下任何内容:

共享插件中不允许

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

共享插件可以导入的模块

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

每次钩子调用有 2,000,000 步解释器额度,和面板渲染相同。不过 on_key 每次按键都会运行,所以在那里做繁重的工作,远在触及上限之前就会拖慢打字。

文件

插件是一份 JSON 文档。id 在更新之间保持不变,version 是显示在列表中的自由文本,icon 是 SF Symbol 名称,source 是转义了换行的脚本。从编辑器的检查器分享它,或用插件标签页的箭头按钮导入一个。

{
  "id": "word-count",
  "name": "Word count",
  "icon": "text.word.spacing",
  "summary": "Counts words on the space bar",
  "version": "1.0",
  "author": "You",
  "enabled": true,
  "source": "def initial():\n    return {\"on\": False}\n..."
}
一个 .clinkplugin 文件,已缩略。

插件以 .clinkplugin 文件共享,这是一份内含脚本的 JSON 文档。插件和面板一样通过仓库发布,仓库里有文件夹、清单和发布版本。官方仓库是 anti-ltd/clink-plugins。

仓库 ›

当某件事没有发生时

多数情况下是以下原因之一。

现象要检查什么
没有任何反应插件标签页顶部的插件功能已关闭、该插件在列表中被关闭,或者没有 Clink Pro 会员。三种情况下键盘都不会运行任何插件。
空格键文字显示为灰色某个插件接管了它。输入框下方那行会标明是哪个;关闭该插件的开关或插件本身,即可取回输入框。
分区没有出现锚点拼写有误。与应用中的“显示 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= 里指名了某个设置,旋钮才会写设置;否则这个值要由插件自己处理,而按钮根本不会写设置。
PyMini ›
在 App Store 下载