#!/usr/bin/env ruby # -*- encoding: utf-8 -*- # Copyright muflax , 2015 # License: GNU GPLv3 (or later) GC.disable # avoid having to require muflax class Object def blank? respond_to?(:empty?) ? !!empty? : !self end end class String BLANK_RE = /\A[[:space:]]*\z/ def blank? BLANK_RE === self end end LayoutDir = "src/keyboard" LayoutFile = "#{LayoutDir}/layout.c" puts "generating #{LayoutFile}..." class Layer @@layers = {} @@primitives = 0 def initialize name case name when Symbol raise "layer already defined: #{name}" if @@layers.include? name @@primitives += 1 @@layers[name] = @@primitives when Array name.each do |prim| raise "unknown layers: #{prim}" if not @@layers.include? prim raise "can only combo primitive layers: #{prim}" if not prim.is_a? Symbol end combo = name.reduce(0) do |code, prim| code | (@@layers[prim]) end combo_name = name.sort_by{|p| @@layers[p]}.join("_").to_sym @@layers[combo_name] = combo else raise "invalid layer definition: #{name}" end end end Keydef = Struct.new :code, :func class Key attr_reader :keydefs, :name, :row, :col Layers = [ :LB, :LP, :LN, :LF, :LD ] Functions = { # "basic" => "&kbfun_normal_press_release", "media" => "&kbfun_mediakey_press_release", # "mod" => "&kbfun_modifier_press_release", "sticky_mod" => "&kbfun_modifier_sticky", # "layer" => "&kbfun_layer_press_release", "sticky" => "&kbfun_layer_sticky", # "shift" => "&kbfun_shift_press_release", "ctrl" => "&kbfun_control_press_release", "alt" => "&kbfun_alt_press_release", "win" => "&kbfun_win_press_release", # "shift_once" => "&kbfun_shift_press_release_once", "ctrl_once" => "&kbfun_control_press_release_once", "alt_once" => "&kbfun_alt_press_release_once", "win_once" => "&kbfun_win_press_release_once", # "shift_layer" => "&kbfun_shift_layer_press_release", "ctrl_layer" => "&kbfun_control_layer_press_release", "alt_layer" => "&kbfun_alt_layer_press_release", "win_layer" => "&kbfun_win_layer_press_release", # "capslock" => "&kbfun_capslock_press_release", # "S" => "&kbfun_shift_press_release", "C" => "&kbfun_control_press_release", "A" => "&kbfun_alt_press_release", "M" => "&kbfun_alt_press_release", "W" => "&kbfun_win_press_release", "CL" => "&kbfun_capslock_press_release", "_L" => "&kbfun_layer_sticky", "_M" => "&kbfun_modifier_sticky", "_C" => "&kbfun_control_layer_press_release", "_A" => "&kbfun_alt_layer_press_release", "_W" => "&kbfun_win_layer_press_release", } Keys = { # letters "a" => "KEY_a_A", "b" => "KEY_b_B", "c" => "KEY_c_C", "d" => "KEY_d_D", "e" => "KEY_e_E", "f" => "KEY_f_F", "g" => "KEY_g_G", "h" => "KEY_h_H", "i" => "KEY_i_I", "j" => "KEY_j_J", "k" => "KEY_k_K", "l" => "KEY_l_L", "m" => "KEY_m_M", "n" => "KEY_n_N", "o" => "KEY_o_O", "p" => "KEY_p_P", "q" => "KEY_q_Q", "r" => "KEY_r_R", "s" => "KEY_s_S", "t" => "KEY_t_T", "u" => "KEY_u_U", "v" => "KEY_v_V", "w" => "KEY_w_W", "x" => "KEY_x_X", "y" => "KEY_y_Y", "z" => "KEY_z_Z", # numbers "0" => "KEY_0_RightParenthesis", "1" => "KEY_1_Exclamation", "2" => "KEY_2_At", "3" => "KEY_3_Pound", "4" => "KEY_4_Dollar", "5" => "KEY_5_Percent", "6" => "KEY_6_Caret", "7" => "KEY_7_Ampersand", "8" => "KEY_8_Asterisk", "9" => "KEY_9_LeftParenthesis", # punctuation "\\" => "KEY_Backslash_Pipe", "{" => ["KEY_LeftBracket_LeftBrace", "shift_once"], "}" => ["KEY_RightBracket_RightBrace", "shift_once"], "[" => "KEY_LeftBracket_LeftBrace", "]" => "KEY_RightBracket_RightBrace", "," => "KEY_Comma_LessThan", "-" => "KEY_Dash_Underscore", "=" => "KEY_Equal_Plus", "`" => "KEY_GraveAccent_Tilde", "." => "KEY_Period_GreaterThan", "\'" => "KEY_SingleQuote_DoubleQuote", ";" => "KEY_Semicolon_Colon", "/" => "KEY_Slash_Question", "~" => ["KEY_GraveAccent_Tilde", "shift_once"], "%" => ["KEY_5_Percent", "shift_once"], "*" => ["KEY_8_Asterisk", "shift_once"], ":" => ["KEY_Semicolon_Colon", "shift_once"], "^" => ["KEY_6_Caret", "shift_once"], "<" => ["KEY_Comma_LessThan", "shift_once"], ">" => ["KEY_Period_GreaterThan", "shift_once"], "?" => ["KEY_Slash_Question", "shift_once"], "!" => ["KEY_1_Exclamation", "shift_once"], "(" => ["KEY_9_LeftParenthesis", "shift_once"], ")" => ["KEY_0_RightParenthesis", "shift_once"], "|" => ["KEY_Backslash_Pipe", "shift_once"], "@" => ["KEY_2_At", "shift_once"], "\"" => ["KEY_SingleQuote_DoubleQuote", "shift_once"], "_" => ["KEY_Dash_Underscore", "shift_once"], "+" => ["KEY_Equal_Plus", "shift_once"], "$" => ["KEY_4_Dollar", "shift_once"], "&" => ["KEY_7_Ampersand", "shift_once"], "#" => ["KEY_3_Pound", "shift_once"], "hash" => ["KEY_3_Pound", "shift_once"], "hsh" => ["KEY_3_Pound", "shift_once"], # enter etc "enter" => "KEY_ReturnEnter", "return" => "KEY_ReturnEnter", "ret" => "KEY_ReturnEnter", "space" => "KEY_Spacebar", "spc" => "KEY_Spacebar", "tab" => "KEY_Tab", "backspace" => "KEY_DeleteBackspace", "bksp" => "KEY_DeleteBackspace", "bs" => "KEY_DeleteBackspace", "delete" => "KEY_DeleteForward", "del" => "KEY_DeleteForward", "home" => "KEY_Home", "hom" => "KEY_Home", "end" => "KEY_End", "page_up" => "KEY_PageUp", "pgup" => "KEY_PageUp", "p-^" => "KEY_PageUp", "page_down" => "KEY_PageDown", "pgdn" => "KEY_PageDown", "p-v" => "KEY_PageDown", "up" => "KEY_UpArrow", "a-^" => "KEY_UpArrow", "down" => "KEY_DownArrow", "a-v" => "KEY_DownArrow", "left" => "KEY_LeftArrow", "a-<" => "KEY_LeftArrow", "right" => "KEY_RightArrow", "a->" => "KEY_RightArrow", "escape" => "KEY_Escape", "esc" => "KEY_Escape", "insert" => "KEY_Insert", "ins" => "KEY_Insert", "menu" => "KEY_Application", "men" => "KEY_Application", # modifiers "alt" => ["MOD_KEY_LeftAlt", "mod" ], "A" => ["MOD_KEY_LeftAlt", "mod" ], "M" => ["MOD_KEY_LeftAlt", "mod" ], "alt_gr" => ["MOD_KEY_RightAlt", "mod" ], "umlaut" => ["MOD_KEY_RightAlt", "mod" ], # "UM" => ["MOD_KEY_RightAlt", "mod" ], "UM" => ["MOD_KEY_RightAlt", "sticky_mod" ], "ctrl" => ["MOD_KEY_LeftControl", "mod" ], "C" => ["MOD_KEY_LeftControl", "mod" ], "control_l" => ["MOD_KEY_LeftControl", "mod" ], "control_r" => ["MOD_KEY_RightControl", "mod" ], "win" => ["MOD_KEY_LeftGUI", "mod" ], "W" => ["MOD_KEY_LeftGUI", "mod" ], "shift_l" => ["MOD_KEY_LeftShift", "mod" ], "S_l" => ["MOD_KEY_LeftShift", "mod" ], "shift_r" => ["MOD_KEY_RightShift", "mod" ], "S_r" => ["MOD_KEY_RightShift", "mod" ], "scroll_lock" => "KEY_ScrollLock", "SCL" => "KEY_ScrollLock", # null "NULL" => "KEY_NULL", "NUL" => "KEY_NULL", "■" => "KEY_NULL", # functions 1..24 "f1" => "KEY_F1", "f2" => "KEY_F2", "f3" => "KEY_F3", "f4" => "KEY_F4", "f5" => "KEY_F5", "f6" => "KEY_F6", "f7" => "KEY_F7", "f8" => "KEY_F8", "f9" => "KEY_F9", "f10" => "KEY_F10", "f11" => "KEY_F11", "f12" => "KEY_F12", "f13" => "KEY_F13", "f14" => "KEY_F14", "f15" => "KEY_F15", "f16" => "KEY_F16", "f17" => "KEY_F17", "f18" => "KEY_F18", "f19" => "KEY_F19", "f20" => "KEY_F20", "f21" => "KEY_F21", "f22" => "KEY_F22", "f23" => "KEY_F23", "f24" => "KEY_F24", # keypad stuff "kp_1" => "KEYPAD_1_End", "kp_2" => "KEYPAD_2_DownArrow", "kp_3" => "KEYPAD_3_PageDown", "kp_4" => "KEYPAD_4_LeftArrow", "kp_5" => "KEYPAD_5", "kp_6" => "KEYPAD_6_RightArrow", "kp_7" => "KEYPAD_7_Home", "kp_8" => "KEYPAD_8_UpArrow", "kp_9" => "KEYPAD_9_PageUp", "kp_0" => "KEYPAD_0_Insert", # "kp_." => "KEYPAD_Period_Delete", "kp_/" => "KEYPAD_Slash", "kp_*" => "KEYPAD_Asterisk", "kp_-" => "KEYPAD_Minus", "kp_+" => "KEYPAD_Plus", "kp_en" => "KEYPAD_ENTER", # mediakeys "MD_PP" => ["MEDIAKEY_PLAY_PAUSE", "media"], "MD_PT" => ["MEDIAKEY_PREV_TRACK", "media"], "MD_NT" => ["MEDIAKEY_NEXT_TRACK", "media"], "MD_S" => ["MEDIAKEY_STOP", "media"], "MD_M" => ["MEDIAKEY_AUDIO_MUTE", "media"], "MD_VU" => ["MEDIAKEY_AUDIO_VOL_UP", "media"], "MD_VD" => ["MEDIAKEY_AUDIO_VOL_DOWN", "media"], "record" => ["MEDIAKEY_RECORD", "media"], "rewind" => ["MEDIAKEY_REWIND", "media"], "eject" => ["MEDIAKEY_EJECT", "media"], "cc_config" => ["MEDIAKEY_CC_CONFIG", "media"], "email" => ["MEDIAKEY_EMAIL", "media"], "calculator" => ["MEDIAKEY_CALCULATOR", "media"], "local_browser" => ["MEDIAKEY_LOCAL_BROWSER", "media"], "bs_search" => ["MEDIAKEY_BROWSER_SEARCH", "media"], "bs_home" => ["MEDIAKEY_BROWSER_HOME", "media"], "bs_b" => ["MEDIAKEY_BROWSER_BACK", "media"], "bs_f" => ["MEDIAKEY_BROWSER_FORWARD", "media"], "bs_stop" => ["MEDIAKEY_BROWSER_STOP", "media"], "bs_refresh" => ["MEDIAKEY_BROWSER_REFRESH", "media"], "bs_bookmarks" => ["MEDIAKEY_BROWSER_BOOKMARKS", "media"], # other weird shit "pause" => "KEY_Pause", "sysreq" => "KEY_SysReq_Attention", "sreq" => "KEY_SysReq_Attention", "SR" => "KEY_SysReq_Attention", # "clear" => "KEY_Clear", # "Clear_Again" => "KEY_Clear_Again", # wut "printscreen" => "KEY_PrintScreen", "prtscr" => "KEY_PrintScreen", "PS" => "KEY_PrintScreen", # "execute" => "KEY_Execute", "exe" => "KEY_Execute", "help" => "KEY_Help", # "menu" => "KEY_Menu", # NOT normal menu! "select" => "KEY_Select", "selct" => "KEY_Select", "sel" => "KEY_Select", "stop" => "KEY_Stop", # "again" => "KEY_Again", "redo" => "KEY_Again", "undo" => "KEY_Undo", # "cut" => "KEY_Cut", "copy" => "KEY_Copy", "paste" => "KEY_Paste", # "find" => "KEY_Find", # "mute" => "KEY_Mute", # "volume_up" => "KEY_VolumeUp", "volume_down" => "KEY_VolumeDown", "vol_up" => "KEY_VolumeUp", "vol_down" => "KEY_VolumeDown", "volumeup" => "KEY_VolumeUp", "volumedown" => "KEY_VolumeDown", "voldown" => "KEY_VolumeDown", "volup" => "KEY_VolumeUp", "voldn" => "KEY_VolumeDown", "vlup" => "KEY_VolumeUp", "vldn" => "KEY_VolumeDown", "vup" => "KEY_VolumeUp", "vdn" => "KEY_VolumeDown", } Names = { # unique key identifiers, must have a name for each of the 80 physical keys # NOTE: the mapping to the matrix is a bit weird, so be careful # name row col "Ln┳o" => [ 5, 0 ], "Ln┳4" => [ 5, 1 ], "Ln┳3" => [ 5, 2 ], "Ln┳2" => [ 5, 3 ], "Ln┳1" => [ 5, 4 ], "Ln┳0" => [ 5, 5 ], "Ln┳d" => [ 5, 6 ], "Lt┳o" => [ 4, 0 ], "Lt┳4" => [ 4, 1 ], "Lt┳3" => [ 4, 2 ], "Lt┳2" => [ 4, 3 ], "Lt┳1" => [ 4, 4 ], "Lt┳0" => [ 4, 5 ], "Lt┳d" => [ 4, 6 ], "Lh┳o" => [ 3, 0 ], "Lh┳4" => [ 3, 1 ], "Lh┳3" => [ 3, 2 ], "Lh┳2" => [ 3, 3 ], "Lh┳1" => [ 3, 4 ], "Lh┳0" => [ 3, 5 ], "Lb┳o" => [ 2, 0 ], "Lb┳4" => [ 2, 1 ], "Lb┳3" => [ 2, 2 ], "Lb┳2" => [ 2, 3 ], "Lb┳1" => [ 2, 4 ], "Lb┳0" => [ 2, 5 ], "Lb┳d" => [ 2, 6 ], "Lu┳o" => [ 1, 0 ], "Lu┳4" => [ 1, 1 ], "Lu┳3" => [ 1, 2 ], "Lu┳2" => [ 1, 3 ], "Lu┳1" => [ 1, 4 ], "Lz┳b" => [ 0, 5 ], "Lz┳c" => [ 0, 6 ], "Ly┳a" => [ 1, 5 ], "Ly┳b" => [ 1, 6 ], "Ly┳c" => [ 0, 4 ], "Lx┳a" => [ 0, 3 ], "Lx┳b" => [ 0, 2 ], "Lx┳c" => [ 0, 1 ], "Rn┳d" => [ 5, 7 ], "Rn┳0" => [ 5, 8 ], "Rn┳1" => [ 5, 9 ], "Rn┳2" => [ 5, 10 ], "Rn┳3" => [ 5, 11 ], "Rn┳4" => [ 5, 12 ], "Rn┳o" => [ 5, 13 ], "Rt┳d" => [ 4, 7 ], "Rt┳0" => [ 4, 8 ], "Rt┳1" => [ 4, 9 ], "Rt┳2" => [ 4, 10 ], "Rt┳3" => [ 4, 11 ], "Rt┳4" => [ 4, 12 ], "Rt┳o" => [ 4, 13 ], "Rh┳0" => [ 3, 8 ], "Rh┳1" => [ 3, 9 ], "Rh┳2" => [ 3, 10 ], "Rh┳3" => [ 3, 11 ], "Rh┳4" => [ 3, 12 ], "Rh┳o" => [ 3, 13 ], "Rb┳d" => [ 2, 7 ], "Rb┳0" => [ 2, 8 ], "Rb┳1" => [ 2, 9 ], "Rb┳2" => [ 2, 10 ], "Rb┳3" => [ 2, 11 ], "Rb┳4" => [ 2, 12 ], "Rb┳o" => [ 2, 13 ], "Ru┳1" => [ 1, 9 ], "Ru┳2" => [ 1, 10 ], "Ru┳3" => [ 1, 11 ], "Ru┳4" => [ 1, 12 ], "Ru┳o" => [ 1, 13 ], "Rz┳c" => [ 0, 7 ], "Rz┳b" => [ 0, 8 ], "Ry┳c" => [ 0, 9 ], "Ry┳b" => [ 1, 7 ], "Ry┳a" => [ 1, 8 ], "Rx┳c" => [ 0, 12 ], "Rx┳b" => [ 0, 11 ], "Rx┳a" => [ 0, 10 ], } Layers.each.with_index do |layer, i| Keys["#{layer}"] = ["#{i}", "layer"] end def initialize layers, name @keydefs = Array.new(layers.size) @name = name @pos = Key::Names[name] @row, @col = @pos if layers.all?(&:empty?) layers[0] = ["NUL"] end layers.each.with_index do |(key, type), i| if type.nil? and key.nil? @keydefs[i] = @keydefs[i-1] next end # keycode and default keyfunc keycode, default_type = Keys[key] # keyfunc type ||= default_type || "basic" keyfunc = Functions[type] raise "overwriting shift key: #{key}, #{type}" if default_type == "shift" and type != default_type raise "key not found: #{key}" if keycode.nil? raise "func not found: #{type}" if keyfunc.nil? case type when "layer", "sticky", "_L" raise "invalid layer: #{key}" unless key.to_i < Key::Layers.size when "mod", "sticky_mod", "_M" raise "invalid modifier: #{key}" unless keycode.start_with? "MOD_KEY" end case keycode when /^MOD_KEY/ case type when "mod", "sticky_mod", "CL", "_M"; # pass else raise "invalid modifier keyfunc: #{key}, #{type}" end end @keydefs[i] = Keydef.new(keycode, keyfunc) end raise "transparency error: #{layers}" if @keydefs.any?(&:nil?) end Dummy = Key.new(Key::Layers.map{"NULL"}, "dummy") end class Layout def initialize name, keys @name = name @keys = keys @rows = @keys.map(&:row).max + 1 @cols = @keys.map(&:col).max + 1 @matrix = @rows.times.map{Array.new(@cols)} @keys.each do |key| @matrix[key.row][key.col] = key end end def keys_to_matrix type, method matrix = [] @matrix.each.with_index do |row, row_i| matrix << "{" row.each.with_index do |key, col_i| key ||= Key::Dummy # some matrix positions are empty matrix << "\t{ // row #{row_i} x col #{col_i}" Key::Layers.each.with_index do |layer_name, layer| matrix << "\t\t(#{type}) #{key.keydefs[layer].send(method)},".ljust(60) + "// #{key.name} on #{layer_name}" end matrix << "\t}," end matrix << "}," end matrix.join("\n") end def save! file puts "saving #{@name}..." header =<
┃ tab _LF ┃ ┃ f14 ┃ f13 ┃ f12 ┃ f11 ┃ f20 _LD ┃ ┃ 4 ┃ 3 ┃ 2 ┃ 1 ┃ 0 ╳ Lb┳o ___ __ Lb┳4 ___ __ Lb┳3 ___ __ Lb┳2 ___ __ Lb┳1 ___ __ Lb┳0 ___ __ Lb┳d ___ __ _LB ┃ S_l CL ┃ % ┃ * ┃ : ┃ p ┃ z ┃ ret _LP ┃ ┃ ` ┃ ^ ┃ | ┃ - ┃ @ ┃ _LN ┃ ┃ hom ┃ p-^ ┃ p-v ┃ end ┃ NUL ┃ _LF ┃ ┃ f18 ┃ f17 ┃ f16 ┃ f15 ┃ f19 ┃ _LD ┃ ┃ 8 ┃ 7 ┃ 6 ┃ 5 ┃ 9 ┃ ╳ Lu┳o ___ __ Lu┳4 ___ __ Lu┳3 ___ __ Lu┳2 ___ __ Lu┳1 ___ __ _LB ┃ a-< ┃ a-^ ┃ a-v ┃ a-> ┃ W _LP ┃ ┃ ┃ ┃ ┃ _LN ┃ ┃ ┃ ┃ ┃ _LF ┃ ┃ ┃ ┃ ┃ _LD ┃ ┃ ┃ ┃ ┃ ╳ Lz┳b ___ __ Lz┳c ___ __ _LB ┃ esc ┃ SCL _LP ┃ ┃ _LN ┃ ┃ _LF ┃ ┃ _LD ┃ ┃ ╳ Ly┳a ___ __ Ly┳b ___ __ Ly┳c ___ __ _LB ┃ spc ┃ C ┃ A _LP ┃ ┃ ┃ _LN ┃ ┃ ┃ _LF ┃ ┃ ┃ _LD ┃ ┃ ┃ ╳ Lx┳a ___ __ Lx┳b ___ __ Lx┳c ___ __ _LB ┃ spc ┃ C ┃ A _LP ┃ ┃ ┃ _LN ┃ ┃ ┃ _LF ┃ ┃ ┃ _LD ┃ ┃ ┃ EOL right =< ┃ " ┃ ] ┃ ` ┃ ` _LN ┃ ┃ 9 ┃ 5 ┃ 6 ┃ 7 ┃ 8 ┃ 8 _LF ┃ ┃ f24 ┃ f21 ┃ f22 ┃ f23 ┃ f24 ┃ f24 _LD ┃ ┃ 9 ┃ 5 ┃ 6 ┃ 7 ┃ 8 ┃ 8 ╳ Rh┳0 ___ __ Rh┳1 ___ __ Rh┳2 ___ __ Rh┳3 ___ __ Rh┳4 ___ __ Rh┳o ___ __ _LB ┃ s ┃ n ┃ r ┃ t ┃ d ┃ UM _LP ┃ ) ┃ _ ┃ / ┃ \} ┃ . ┃ _LN ┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃ 4 ┃ _LF ┃ f10 ┃ f1 ┃ f2 ┃ f3 ┃ f4 ┃ _LD ┃ 0 ┃ 1 ┃ 2 ┃ 3 ┃ 4 ┃ ╳ Rb┳d ___ __ Rb┳0 ___ __ Rb┳1 ___ __ Rb┳2 ___ __ Rb┳3 ___ __ Rb┳4 ___ __ Rb┳o ___ __ _LB ┃ ret ┃ b ┃ m ┃ j ┃ y ┃ ; ┃ S_r CL _LP ┃ ┃ + ┃ $ ┃ & ┃ hsh ┃ ^ ┃ _LN ┃ ┃ 9 ┃ 5 ┃ 6 ┃ 7 ┃ 8 ┃ _LF ┃ ┃ f9 ┃ f5 ┃ f6 ┃ f7 ┃ f8 ┃ _LD ┃ ┃ 9 ┃ 5 ┃ 6 ┃ 7 ┃ 8 ┃ ╳ Ru┳1 ___ __ Ru┳2 ___ __ Ru┳3 ___ __ Ru┳4 ___ __ Ru┳o ___ __ _LB ┃ LN ┃ a-< ┃ a-^ ┃ a-v ┃ a-> _LP ┃ ┃ ┃ ┃ ┃ _LN ┃ ┃ ┃ ┃ ┃ _LF ┃ ┃ ┃ ┃ ┃ _LD ┃ ┃ ┃ ┃ ┃ ╳ Rz┳c ___ __ Rz┳b ___ __ _LB ┃ LF ┃ LF _C _LP ┃ ┃ _LN ┃ ┃ _LF ┃ ┃ _LD ┃ ┃ ╳ Ry┳c ___ __ Ry┳b ___ __ Ry┳a ___ __ _LB ┃ men ┃ LF ┃ LP _L _LP ┃ ┃ ┃ _LN ┃ ┃ ┃ _LF ┃ ┃ ┃ _LD ┃ ┃ ┃ ╳ Rx┳c ___ __ Rx┳b ___ __ Rx┳a ___ __ _LB ┃ men ┃ LF ┃ LP _L _LP ┃ ┃ ┃ _LN ┃ ┃ ┃ _LF ┃ ┃ ┃ _LD ┃ ┃ ┃ EOL keys_string = [ left, right, ] groups = keys_string.flat_map do |str| str.gsub(/#.*$/, '').split(/╳/).reject(&:blank?).map(&:lines) end keys_parsed = {} groups.each.with_index(1) do |group, group_id| layers = Key::Layers.size # extract names names = group.first names = names.scan(/(\S+)(?:[ ]*\t[^\t]*){2}/).map{|name, _| name.strip} keyrow = group[1..-1].reject(&:blank?) # sanity check if keyrow.size != layers raise "wrong number of key layers in group ##{group_id}: #{keyrow}" end # remove first label keyrow = keyrow.map{|l| l.split("┃").drop(1)} (0..(keyrow.size-1)).step(layers).each do |row| keyrow[row].size.times do |col| key = layers.times.map do |layer| keyrow[row+layer][col] end key = key.map{|l| l.strip.split(/\s+/, 2)} name = names[col] raise "name not unique: #{name}" if keys_parsed.include? name raise "unknown name: #{name}" unless Key::Names.include? name keys_parsed[name] = key end end end keys = keys_parsed.map {|name, layers| Key.new(layers, name)} saneo = Layout.new :saneo, keys saneo.save! LayoutFile