자동화

.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)키보드가 화면에 있는 동안, 입력 여부와 상관없이 1초에 한 번. 시계, 카운트다운, 멈추면 0으로 돌아가야 하는 속도처럼 스스로 변해야 하는 것을 위한 훅입니다.
on_swipe(direction, state)글자 키에서 시작하는 쓸어 넘기기로, 방향은 "left", "right", "up", "up_left", "up_right" 중 하나입니다. 스와이프 입력이 꺼져 있을 때만 읽습니다. 훅이 아무것도 요청하지 않으면 평범한 키 입력으로 남고, 요청하면 시작한 글자를 먼저 되돌립니다.
elements(state)이 플러그인이 사용자 레이아웃에 제공하는 것. element(id, name, icon=, width=) 항목을 반환하거나, 훅을 생략합니다.
draw(id, state)엘리먼트 하나의 표시 내용을 노드 트리로 반환합니다. 키보드가 떠 있는 동안 약 1초에 한 번 실행됩니다.
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로 상태에 쓰거나, 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은 Return이 실행할 처리기의 이름입니다.
button(label, action="", value=None, insert="", set=None, style="plain", icon="", enabled=True)버튼입니다. insert는 지금 쓰고 있는 곳에 그 글자를 입력하고, set은 상태에 키를 합치며, action은 on_action이 받을 이름을 지정합니다. style은 plain, primary, tinted, quiet, destructive를 받습니다.
row(title, subtitle="", detail="", icon="", action="", value=None, insert="")탭할 수 있는 행입니다. 제목, 둘째 줄, 아이콘, 오른쪽 부가 정보로 이루어집니다.
text(s, size=17, weight="regular", color="", align="leading", lines=0, mono=False)한 줄의 텍스트입니다. weight는 regular, medium, semibold, bold, heavy, light, thin을 받고, align은 leading, center, trailing을 받습니다. lines는 줄바꿈 줄 수를 제한하고, color는 색 이름이나 #RRGGBB를 받습니다.
패널 ›

섹션이 놓이는 곳

앵커는 Clink 자체 설정 화면에 있는 카드의 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)같은 배지를 지역의 깃발 이모지로 보여 줍니다. 배지 명령 세 개는 자리를 하나만 나눠 쓰므로, 배지 플러그인 하나를 켜면 나머지는 꺼집니다.
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"]최근 몇 초 동안의 분당 단어 수로, 입력란에 도달한 문자로 셉니다. 치기 시작하고 1~2초 뒤에 나타나고, 쉬는 동안 내려가며, 멈추면 0이 됩니다. 지우기로는 절대 늘지 않습니다.
stats()["peak_wpm"]분석에 기록된 역대 최고 속도입니다.
stats()["keystrokes"]분석이 세는 누적 키 입력 수입니다.
stats()["words"]누적 확정 단어 수입니다.
stats()["streak"]오늘까지 이어진 연속 입력 일수입니다.

setting(name)과 set_setting(name, value)은 같은 이름 목록을 공유하며, claim(control)도 그중 무엇이든 받습니다. 불리언은 True나 False로, 숫자는 숫자로, 선택지는 그 id로 읽힙니다. 목록이 긴 것은 의도된 것입니다. 플러그인은 사람이 앱에서 설정할 수 있는 거의 모든 것에 반응하거나 그것을 제어할 수 있습니다.

setting()에 쓸 수 있는 이름

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

명령

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

패널의 view()처럼 settings()도 컨트롤을 기술하기만 해야 합니다. 거기서 호출한 명령은 무시되고 콘솔에 기록됩니다.

예시

작은 플러그인 다섯 개, 각각 완결되어 있습니다. 새 플러그인에 붙여 넣고 저장하면 바로 실행됩니다.

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

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

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

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

def on_language(code, state):
    if state["on"]:
        space_text(code.upper())
    return state
언어 배지: 스페이스바에 입력 언어를 표시하고, 바뀌는 순간 갱신합니다.
SHORTCUTS = {"omw": "on my way", "brb": "be right back", "ty": "thank you"}

def on_key(key, state):
    if key != " ":
        return state
    words = context()["before"].split()
    if words and words[-1] in SHORTCUTS:
        # the space is already in the text, so it goes too and comes back after
        replace(len(words[-1]) + 1, SHORTCUTS[words[-1]] + " ")
    return state
단축어: omw와 스페이스를 입력하면 on my way가 됩니다. on_key는 스페이스가 입력된 뒤에 보므로, 치환이 그 스페이스까지 넘어갑니다.
def initial():
    return {"goal": 200, "count": 0}

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

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

def on_word(word, state):
    state["count"] += 1
    if state["count"] == state["goal"]:
        haptic("medium")
        banner("Goal reached")
    return state
단어 목표: 플러그인 페이지의 스테퍼와 진행 막대, 세션이 목표에 닿으면 진동과 배너로 알립니다.
def initial():
    return {"muted": False, "was_on": True}

def on_field(kind, state):
    # a password or a number field is not a place for key sounds
    quiet = kind in ("password", "number", "phone")
    if quiet and not state["muted"]:
        state["was_on"] = setting("sound.enabled")
        state["muted"] = True
        set_setting("sound.enabled", False)
    elif not quiet and state["muted"]:
        state["muted"] = False
        if state["was_on"]:
            set_setting("sound.enabled", True)
    return state
조용한 입력란: 비밀번호나 숫자 입력란에서는 키 소리를 끄고 그 뒤에 되돌립니다. on_field, setting(), set_setting()을 씁니다.
NAMES = ["sam", "ana", "team"]

def on_key(key, state):
    typing = context()["before"].split(" ")[-1]
    if typing.startswith("@"):
        start = typing[1:].lower()
        suggest([n for n in NAMES if n.startswith(start)])
        state["showing"] = True
    elif state.get("showing"):
        suggest([])                 # the mention is over, give the bar back
        state["showing"] = False
    return state
멘션: @를 입력하면 내가 정한 이름들이 제안 막대에 올라옵니다.
def on_swipe(direction, state):
    if direction == "left":
        delete_word()
    elif direction == "right":
        press("space")              # the space bar's own path, so it autocorrects
    elif direction == "up":
        pick_suggestion("center")
    elif direction == "up_left":
        pick_suggestion("left")
    elif direction == "up_right":
        pick_suggestion("right")
    return state
Fleksy 방식의 쓸어 넘기기로, Flick Gestures 플러그인의 핵심입니다. 왼쪽은 단어 삭제, 오른쪽은 공백 입력, 위쪽은 제안 선택입니다. 스와이프 입력이 꺼져 있을 때만 전달됩니다.

레이아웃 속 엘리먼트

사용자 레이아웃은 키와 엘리먼트로 만듭니다. 최근 이모지 줄, 숫자 줄, 커서 패드 같은 것들이죠. 플러그인은 자기 것을 더할 수 있습니다. elements(state)가 무엇을 제공하는지 알리고 draw(id, state)가 그것을 그리므로, 플러그인이 계산할 수 있는 것이면 무엇이든 키에 띄울 수 있습니다. 타자 속도 그래프, 시계, 카운트다운, 직접 만든 배터리 표시까지.

훅 두 개로 하나를 만듭니다. elements(state)는 제공하는 것을 엘리먼트마다 하나씩 나열하고, 앱은 이것을 읽어 레이아웃 편집기 팔레트를 채웁니다. draw(id, state)는 그 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 플러그인 전체입니다. 엘리먼트 하나를 제공하고, 거기에 최근 30초의 속도를 그립니다.

엘리먼트의 화면은 설정 페이지와 같은 노드 트리지만, 키는 설정 페이지가 아닙니다. 그리는 노드만 쓰이고 탭할 수 있는 것은 무시됩니다. 키는 이미 레이아웃의 것이라 그 안에 버튼을 둘 자리가 없습니다.

엘리먼트가 그릴 수 있는 것

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

키 크기에 맞춰 설계하세요. width는 키 너비 단위로, 1은 글자 키 하나, 3은 한 줄의 3분의 1쯤이며 나중에 사용자가 조절할 수 있습니다. 텍스트는 한 줄이고 들어가도록 줄어듭니다. 색은 기본적으로 키의 글자색을 따르므로, 따로 지정하지 않으면 주변 키와 어울립니다. 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")
키 위의 시계입니다. 상태도 훅도 없이, 물어볼 때마다 현재 시각만 돌려줍니다. 엘리먼트는 대략 1초에 한 번 스스로 다시 그려집니다.
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=는 파이썬으로 모양을 그립니다.
def initial():
    return {"width": 2.0}

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

def on_action(action, value, state):
    if action == "sig":
        insert(" sent from Clink")
    elif action == "width":
        state["width"] = value
    return state
버튼 하나와 노브 둘, 그중 하나는 키 음량에 묶여 있습니다

둘 다 on_action(action, value, state)으로 돌아오며 항목 id로 구분되므로, 플러그인의 바 항목과 설정 컨트롤이 같은 훅을 씁니다. 탭은 None을 보냅니다. 노브는 손가락이 닿아 있는 동안 걸림마다 값을 보내고, 놓을 때 한 번 더 보내므로 플러그인은 드래그를 따라가거나 마지막 값만 기다릴 수 있습니다.

setting=을 붙인 노브는 앱이 이미 가진 값을 돌립니다. 이름은 위의 명령과 읽기 목록에 있는 것, 예를 들어 sound.volume이나 haptics.intensity입니다. 앱이 모르는 이름은 콘솔에서 KeyError가 됩니다. 범위와 시작 위치는 그 설정에서 옵니다. 드래그하는 동안 키보드 자신의 사본이 바뀌므로 다음 타건은 이미 새 값으로 울리고, 값은 손을 뗄 때 저장됩니다. 음량이나 햅틱 노브를 0에서 올리면 드래그하는 동안 소리나 햅틱이 다시 켜져서, 올라가는 길의 걸림이 들리고 느껴집니다. 그 스위치를 저장하는 것은 플러그인이 on_action 안에서 직접 부르는 set_setting입니다.

노브는 파이썬으로 그릴 수 있습니다. 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는 색상이 순환하는 최대 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"),
        ]),
    ]
일정한 밝기의 바탕 위에 세 가지 색을 지나는 느린 물결을 얹은 예. 물결은 바탕에 빛을 더하면서 색을 옮깁니다.

패턴

  • 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. 각각 아래 호출로 만든 항목을 반환하고, 이 항목들은 기본 선택지 옆의 플러그인에서에 나타납니다. 단, 테마는 테마 편집기의 별도 플러그인 탭에 나타납니다. 모양은 그리기 코드가 아니라 숫자와 단어라서 스크립트는 프레임마다 실행되지 않습니다. 하나를 고르면 사용자의 설정에 복사되므로 플러그인을 꺼도 계속 작동하고, 나중에 기본 선택지를 고르면 옆으로 치워집니다.

호출하는 일
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)])]
무지개 스와이프 궤적

모양은 고를 때 복사되므로, 나중에 스크립트를 바꿔도 이미 쓰고 있는 모양은 바뀌지 않습니다. 다시 골라야 합니다. 철자가 틀린 키워드, 목록에 없는 단어, 숫자 자리에 들어간 텍스트는 편집기 콘솔의 오류가 되며, 숫자는 앱의 컨트롤과 같은 범위로 제한됩니다.

키에 그리는 그림

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는 16진 색, "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)칠: 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)도형 아래 그림자로, 최대 세 개까지. 키를 가득 채우는 옅은 그림자는 키 모서리를 각지게 자르지 않고 따라가고, 글로는 키 밖으로 번집니다.

그림은 플러그인이 듣는 이벤트마다 다시 읽힙니다. 고리는 이게 전부입니다. on_event(name, info, state)에서 상태를 바꾸고, key_art(state)에서 그 상태로 그립니다. events(state)는 원하는 이벤트 이름을 적는 곳이고 플러그인을 불러올 때 한 번 읽습니다. 이게 없으면 key, key_down, key_up, predictions, tick을 뺀 나머지를 모두 듣습니다. 이 다섯은 타건마다 또는 초마다 스크립트를 돌리므로 이름으로 청해야 합니다. 시프트와 면이 바뀌면 듣는 쪽이 없어도 그림을 다시 그리고, context()에도 들어옵니다.

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

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

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

def key_art(state):
    if state["shift"] != "locked":
        return {}
    lamp = shape("circle", anchor="top_right", x=-7, y=7, size=5,
                 fill="accent", shadow=shadow("accent", radius=4))
    return {"shift": art([lamp], animate=0.12)}
캡스록 램프. 키보드에 들어 있는 기능이 아니라 플러그인이 그린 것입니다

도형

  • 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"가 쓰입니다. 플러그인이 적지 않은 키는 사용자 자신의 설정을 유지합니다. 두 표 모두 키보드가 열릴 때와 그 뒤 약 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 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 문서인 .clinkplugin 파일로 공유됩니다. 배포 방식은 패널과 같아서, 파일 폴더와 매니페스트, 릴리스가 있는 저장소를 통해 게시합니다. 공식 저장소는 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에서 다운로드