オートメーション

.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()保存された状態がまだないときに一度だけ呼ばれます。JSON で保存できる値からなる dict を返します。
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 です。キーボードを開いたときと、その後およそ1秒ごとに読み込まれます。
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 を受け取り、変更して返します。数値、文字列、リスト、入れ子の dict など、JSON に入るものなら何でも持てます。アプリはコントロールを使うたびに保存し、キーボードは閉じるときに保存し、どちらも同じファイルを読むので、長くずれることはありません。

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 は参照なので、その場で書き換えても動きます。上の initial() のように別の dict を返せば、それが状態になります。

設定とセクション

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 で state に書き込むか、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="")リストから 1 つを選ばせます。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 は state にキーを差し込み、action は on_action の受け口の名前になります。style は plain、primary、tinted、quiet、destructive を取ります。
row(title, subtitle="", detail="", icon="", action="", value=None, insert="")タップできる行です。タイトル、2 行目、アイコン、右端の補足からなります。
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)スペースバーの角にある言語バッジを上書きします。単独で使え、標準のバッジがオフでも効きます。1行、最大12文字。"" で隠し、None で標準の文字に戻します。
space_language_flag(language)そのバッジに、en_GB のような言語 id の旗を、同梱の旗の絵から表示します。None で標準の動作に戻ります。
space_language_emoji(language)同じバッジを、地域の旗の絵文字で表示します。3つのバッジコマンドは1つの枠を共有するので、どれか1つのバッジプラグインをオンにすると他はオフになります。
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 は 0 より大きいと目盛りに吸い付き、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
ボタン1つとノブ2つ。うち1つはキー音量に結び付いています

どちらも on_action(action, value, state) に戻ってきます。手がかりは項目の id なので、プラグインのバー項目と設定のコントロールは同じフックを共有します。タップでは None が渡されます。ノブは指が触れているあいだ目盛りごとに値を送り、離したときにもう一度送るので、ドラッグに追従することも、最後の値だけを待つこともできます。

setting= を付けたノブは、アプリがすでに持っている値を回します。名前は上の「コマンドと読み取り」の一覧にあるもの、たとえば sound.volume や haptics.intensity です。アプリが知らない名前はコンソールで KeyError になります。範囲も初期位置もその設定から来ます。ドラッグ中はキーボード自身の控えが変わるので、次の打鍵はもう新しいレベルで鳴り、値は指を離したときに保存されます。音量や触覚のノブをゼロから上げると、ドラッグのあいだだけ音や触覚が戻るので、上げていく途中の目盛りが聞こえ、感じられます。このスイッチを保存するのはプラグイン自身で、on_action の中の set_setting です。

ノブは Python で描けます。art= は動かない部分、ベゼルや本体で、rotor= は回る面です。最小で -135 度、最大で +135 度まで回ります。どちらもキーアートと同じ図形、塗り、グラデーション、影を受け取り、それぞれ最大16図形まで使えます。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 は色が巡る最大8個の "#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"),
        ]),
    ]
一定の明るさのベースに、3色を巡るゆっくりしたウェーブを重ねた例。ウェーブはベースに光を足しながら色を動かします。

パターン

  • 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=10から4まで。0ではパターンが止まります。
size=10.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 がないので「色」の設定に従い、スペクトラムなら虹色になります。

エフェクトを選ぶと利用者の設定にコピーされるので、プラグインをオフにしても動き続けます。プラグインがオンでそのエフェクトが動いている間、キーボードは開いたときとその後およそ1秒ごとに 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 になり、レイヤーのないエフェクトは除外されます。

外観

key_styles、themes、popups、animations、backgrounds、trails、layouts の7つのフックは、アプリに選択肢を提供します。それぞれ下の呼び出しで作ったエントリーを返し、それらは組み込みの選択肢の隣の「プラグインから」に表示されます。ただしテーマだけは、テーマエディタの専用の「プラグイン」タブに表示されます。外観は描画コードではなく数値と単語なので、スクリプトがフレームごとに動くことはありません。選ぶと利用者の設定にコピーされるため、プラグインをオフにしても動き続け、その後に組み込みの選択肢を選ぶと脇に置かれます。

呼び出し動作
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=[...])描いたキー面の1レイヤー。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(...) のレイヤーを最大4つ、"#rrggbb" の色を最大8つ持てます。
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 でスペースバーの横に最大3つのキーを置けます。選ぶと普通のカスタムレイアウトとしてインストールされます。
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)])]
虹色のスワイプ軌跡

外観は選んだ時点でコピーされるので、あとでスクリプトを変えても使用中の外観は変わりません。選び直してもらう必要があります。キーワードのつづり間違い、リストにない単語、数値の位置の文字列はエディタのコンソールでエラーになり、数値はアプリ自身のコントロールと同じ範囲に収められます。

キーに描くアート

key_art(state) はキー名から絵への dict を返し、キーボードがそれをキーの中に描きます。キー名は haptics と同じで、letters と keys のフォールバックも効きます。絵は shape(...) のリスト、単体の図形、art([...]) のレイヤー、またはレイヤーのリストです。各レイヤーは自分の時計で次の絵へ移るので、1つのキーに速さの違う2つのものを載せられます。

呼び出し動作
art(shapes, animate=0, curve="ease_out")アニメーションする1レイヤー。新しい絵を渡すと、animate 秒かけて curve に沿って移ります。curve は ease_out、linear、ease_in、ease_in_out、spring のいずれかです。1つのキーに複数のレイヤーを置くと、それぞれが別々に動きます。
shape(kind, anchor="center", unit="pt", x=0, y=0, size=, width=, height=, fill=, stroke=, ...)描かれるもの1つ。circle、rect、capsule、line、path、text、icon があります。キー上の anchor から x と y の位置に置かれ、単位はポイント、unit="key" ならキーに対する割合です。fill と stroke は16進カラー、"text"、"accent"、color(...)、gradient(...) を受け取り、さらに line_width、corner、trim_from、trim_to、rotation、opacity、blur、影は3つまで指定できます。
graph(values, min=None, max=None, width=0.8, height=0.25, stroke=, fill=)数値の並びを普通の図形に展開したもの。図形リストにつなげても、単独で使っても構いません。直近60個の有限なサンプルが残り、範囲を指定しなければ並び自体から決まります。fill は線の下にベースラインまでの閉じたパスを足します。
color(value, opacity=1)塗り。16進カラー、または落ちたキーに合わせて解決される "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)図形の下の影で、3つまで置けます。キーを満たすほどの広い影はキーの角を四角く切らずに沿い、グローはキーの外へ漏れます。

絵はプラグインが受け取ったイベントのたびに読み直されます。これが全体のループです。on_event(name, info, state) で状態を変え、key_art(state) でそこから描く。events(state) は欲しいイベント名を並べるもので、プラグインの読み込み時に一度だけ読まれます。これがないと、key、key_down、key_up、predictions、tick 以外はすべて届きます。この5つは打鍵ごと、あるいは毎秒スクリプトを走らせるので、名指しで頼む必要があります。シフトと面の変化は誰も聞いていなくても絵を描き直し、context() にも届きます。

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

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

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

def key_art(state):
    if state["shift"] != "locked":
        return {}
    lamp = shape("circle", anchor="top_right", x=-7, y=7, size=5,
                 fill="accent", shadow=shadow("accent", radius=4))
    return {"shift": art([lamp], animate=0.12)}
Caps Lock のランプ。キーボードの組み込み機能ではなく、プラグインが描いています

図形

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

1キーあたり図形は16個まで(そのキーの全レイヤー合計)、1パスあたり点は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" が使われます。プラグインが挙げていないキーは利用者自身の設定のままです。どちらの表もキーボードを開いたときと、その後およそ1秒ごとに読み込まれるので、キー入力ごとに何かが動くことはありません。

呼び出し動作
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 Symbols の名前、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 に移すか、処理を減らすか、計算結果を state にキャッシュしてください。
「プラグインから」にエフェクトが出ないプラグイン機能がオンで、そのプラグインが有効であり、effects(state) がレイヤーを1つ以上持つ light_effect を返している必要があります。エディタで effects をタップして返ってきた内容を確かめ、コンソールに ValueError がないか見てください。
トップバーの項目を使っても何も起きないタップも、離したノブも、どちらも on_action(action, value, state) に届きます。照合は項目の名前ではなく id です。ノブが設定を書き込むのは bar_knob の setting= で名前を挙げたときだけで、そうでなければ値の扱いはプラグインの仕事です。ボタンは設定を書き込むことがありません。
PyMini ›
App Storeでダウンロード