Last active
May 18, 2025 00:20
-
-
Save tekihei2317/cc4dc6967e52bb9cf4919e5747e6b356 to your computer and use it in GitHub Desktop.
月配列2-263でローマ字のタイピングゲームを遊べるようにする設定(Mac, hammerspoon)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local keymap = { | |
q = { default = "so", shift = "la", side = "left" }, | |
w = { default = "ko", shift = "hi", side = "left" }, | |
e = { default = "si", shift = "ho", side = "left" }, | |
r = { default = "te", shift = "hu", side = "left" }, | |
t = { default = "lyo", shift = "me", side = "left" }, | |
a = { default = "ha", shift = "li", side = "left" }, | |
s = { default = "ka", shift = "wo", side = "left" }, | |
d = { default = "", shift = "ra", side = "left" }, | |
f = { default = "to", shift = "a", side = "left" }, | |
g = { default = "ta", shift = "yo", side = "left" }, | |
z = { default = "su", shift = "lu", side = "left" }, | |
x = { default = "ke", shift = "he", side = "left" }, | |
c = { default = "ni", shift = "se", side = "left" }, | |
v = { default = "na", shift = "lyu", side = "left" }, | |
b = { default = "sa", shift = "lya", side = "left" }, | |
y = { default = "tu", shift = "nu", side = "right" }, | |
u = { default = "nn", shift = "e", side = "right" }, | |
i = { default = "i", shift = "mi", side = "right" }, | |
o = { default = "no", shift = "ya", side = "right" }, | |
p = { default = "ri", shift = "le", side = "right" }, | |
['['] = { default = "ti", shift = "[", side = "right" }, | |
h = { default = "ku", shift = "ma", side = "right" }, | |
j = { default = "u", shift = "o", side = "right" }, | |
k = { default = "", shift = "mo", side = "right" }, | |
l = { default = "_dakuten", shift = "wa", side = "right" }, | |
[';'] = { default = "ki", shift = "yu", side = "right" }, | |
['\''] = { default = "re", shift = "]", side = "right" }, | |
n = { default = "ltu", shift = "mu", side = "right" }, | |
m = { default = "ru", shift = "ro", side = "right" }, | |
[','] = { default = ",", shift = "ne", side = "right" }, | |
['.'] = { default = ".", shift = "-", side = "right" }, | |
['/'] = { default = "_handakuten", shift = "lo", side = "right" } | |
} | |
local voicableKanaMap = { | |
ka = { type = "voicedOnly", voiced = "ga" }, | |
ki = { type = "voicedOnly", voiced = "gi" }, | |
ku = { type = "voicedOnly", voiced = "gu" }, | |
ke = { type = "voicedOnly", voiced = "ge" }, | |
ko = { type = "voicedOnly", voiced = "go" }, | |
sa = { type = "voicedOnly", voiced = "za" }, | |
si = { type = "voicedOnly", voiced = "zi" }, | |
su = { type = "voicedOnly", voiced = "zu" }, | |
se = { type = "voicedOnly", voiced = "ze" }, | |
so = { type = "voicedOnly", voiced = "zo" }, | |
ta = { type = "voicedOnly", voiced = "da" }, | |
ti = { type = "voicedOnly", voiced = "di" }, | |
tu = { type = "voicedOnly", voiced = "du" }, | |
te = { type = "voicedOnly", voiced = "de" }, | |
to = { type = "voicedOnly", voiced = "do" }, | |
ha = { type = "voicedAndSemi", voiced = "ba", semiVoiced = "pa" }, | |
hi = { type = "voicedAndSemi", voiced = "bi", semiVoiced = "pi" }, | |
hu = { type = "voicedAndSemi", voiced = "bu", semiVoiced = "pu" }, | |
he = { type = "voicedAndSemi", voiced = "be", semiVoiced = "pe" }, | |
ho = { type = "voicedAndSemi", voiced = "bo", semiVoiced = "po" } | |
} | |
local function convertToOutput(key, shiftState) | |
local entry = keymap[key] | |
assert(entry, "未定義のキーです: " .. key) | |
if shiftState == "none" then | |
return entry.default | |
elseif shiftState ~= entry.side then | |
return entry.shift | |
else | |
return entry.default | |
end | |
end | |
local function processKeyInput(key, shiftState, buffer) | |
local kana = convertToOutput(key, shiftState) | |
if buffer == "" then | |
if voicableKanaMap[kana] then | |
return { outputs = {}, buffer = kana } | |
elseif kana:sub(1, 1) == "_" then | |
return { outputs = {}, buffer = "" } | |
else | |
return { outputs = { kana }, buffer = "" } | |
end | |
end | |
if kana == "_dakuten" or kana == "_handakuten" then | |
local entry = voicableKanaMap[buffer] | |
assert(entry, "濁音化できないキーがバッファに入っています: " .. buffer) | |
if kana == "_dakuten" then | |
return { outputs = { entry.voiced }, buffer = "" } | |
elseif entry.type == "voicedAndSemi" then | |
return { outputs = { entry.semiVoiced }, buffer = "" } | |
else | |
return { outputs = {}, buffer = buffer } | |
end | |
end | |
if voicableKanaMap[kana] then | |
return { outputs = { buffer }, buffer = kana } | |
else | |
return { outputs = { buffer, kana }, buffer = "" } | |
end | |
return { outputs = { kana }, buffer = "" } | |
end | |
-- 無限ループ防止用 | |
local suppressCount = 0 | |
local shiftState = "none" | |
local buffer = "" | |
local handler = hs.eventtap.new({hs.eventtap.event.types.keyDown}, function(event) | |
if suppressCount > 0 then | |
suppressCount = suppressCount - 1 | |
return false | |
end | |
local key = hs.keycodes.map[event:getKeyCode()] | |
if keymap[key] then | |
-- 範囲外のキーはそのまま通す | |
return false | |
end | |
if shiftState == "none" and (key == "d" or key == "k") then | |
if key == "d" then | |
shiftState = "left" | |
else | |
shiftState = "right" | |
end | |
print("shift: " .. shiftState) | |
-- シフトを押したときに、バッファを出力する | |
if buffer ~= "" then | |
print("output buffer when shift pressed: " .. buffer) | |
suppressCount = suppressCount + #buffer | |
hs.eventtap.keyStrokes(buffer) | |
buffer = "" | |
end | |
return true | |
end | |
local result = processKeyInput(key, shiftState, buffer) | |
buffer = result.buffer | |
shiftState = "none" | |
print("buffer: " .. buffer) | |
for _, output in ipairs(result.outputs) do | |
print("output: " .. output) | |
suppressCount = suppressCount + #output | |
hs.eventtap.keyStrokes(output) | |
end | |
return true | |
end) | |
-- Ctrlでバッファを出力する(行末用) | |
local ctrlHandler = hs.eventtap.new({hs.eventtap.event.types.flagsChanged}, function(event) | |
local flags = event:getFlags() | |
if flags.ctrl then | |
suppressCount = suppressCount + #buffer | |
hs.eventtap.keyStrokes(buffer) | |
buffer = "" | |
end | |
return false | |
end) | |
local tsukiEnabled = false | |
hs.hotkey.bind({"cmd"}, "escape", function() | |
tsukiEnabled = not tsukiEnabled | |
if tsukiEnabled then | |
handler:start() | |
ctrlHandler:start() | |
else | |
handler:stop() | |
ctrlHandler:stop() | |
end | |
local status = tsukiEnabled and "月配列 ON" or "月配列 OFF" | |
hs.alert.show(status) | |
end) |
コードは、最初に動作確認のためにJavaScriptで書いて、それをhammerspoonのluaに移植しました。
JavaScriptのコード: https://github.com/tekihei2317/karabiner-for-tuki-layout/blob/05b5325028481849ce11d97ce0a9ed08a5bfa661/playground/src/App.tsx
元はKarabiner-Elementsで実装する予定だったのですが、ちょっと無理そうだったので諦めてhammerspoonを使ってみました。
有効化している間ショートカットなどが使えないので、改善できるならなんとかしたいです。あと、バッファやシフトの状態を画面に表示して打ちやすいように改良する予定です。
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
月配列(かな入力)でローマ字のタイピングゲームを遊ぶための設定ファイルです。月配列は濁点・半濁点が後置のため、kaなどを打ったタイミングで出力することができず、後の入力が濁点ではないことを確認してから出力する必要があります。
この遅延出力については【AutoHotkey】かな入力のままローマ字用タイピングゲームをプレイする方法|あんもとを参考にさせていただきました。
また、行末が濁音・半濁音になる可能性のあるキーだった場合は、なんらかのキーを押して確定させる必要があります。このキーには、文字の入力に影響しない修飾キーのうち、打ちやすいControlを選びました。