Skip to content

Instantly share code, notes, and snippets.

@tekihei2317
Last active May 18, 2025 00:20
Show Gist options
  • Save tekihei2317/cc4dc6967e52bb9cf4919e5747e6b356 to your computer and use it in GitHub Desktop.
Save tekihei2317/cc4dc6967e52bb9cf4919e5747e6b356 to your computer and use it in GitHub Desktop.
月配列2-263でローマ字のタイピングゲームを遊べるようにする設定(Mac, hammerspoon)
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)
@tekihei2317
Copy link
Author

tekihei2317 commented May 17, 2025

月配列(かな入力)でローマ字のタイピングゲームを遊ぶための設定ファイルです。月配列は濁点・半濁点が後置のため、kaなどを打ったタイミングで出力することができず、後の入力が濁点ではないことを確認してから出力する必要があります。

この遅延出力については【AutoHotkey】かな入力のままローマ字用タイピングゲームをプレイする方法|あんもとを参考にさせていただきました。

また、行末が濁音・半濁音になる可能性のあるキーだった場合は、なんらかのキーを押して確定させる必要があります。このキーには、文字の入力に影響しない修飾キーのうち、打ちやすいControlを選びました。

@tekihei2317
Copy link
Author

tekihei2317 commented May 17, 2025

コードは、最初に動作確認のために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