├── .gitignore ├── LICENSE ├── MacTcode.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── xcshareddata │ └── xcschemes │ │ └── MacTcode.xcscheme └── xcuserdata │ └── maeda.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── MacTcode ├── Action.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 512.png │ │ ├── 64.png │ │ └── Contents.json │ ├── Contents.json │ ├── MenuIcon.imageset │ │ ├── Contents.json │ │ ├── black16.png │ │ ├── black32.png │ │ ├── white16.png │ │ └── white32.png │ ├── MenuIconDark.imageset │ │ ├── Contents.json │ │ ├── white16.png │ │ └── white32.png │ └── MenuIconLight.imageset │ │ ├── Contents.json │ │ ├── black16.png │ │ └── black32.png ├── Bushu │ ├── Bushu.swift │ └── bushu.dic ├── Client │ ├── Client.swift │ ├── ClientWrapper.swift │ ├── ContextClient.swift │ ├── RecentTextClient.swift │ └── YomiContext.swift ├── Command.swift ├── Config.swift ├── Info.plist ├── Keymap │ ├── EmitPendingAction.swift │ ├── InputEvent.swift │ ├── Keymap.swift │ ├── KeymapResolver.swift │ ├── MultiStroke.swift │ ├── RemoveLastPendingAction.swift │ ├── ResetAllStateAction.swift │ └── Translator.swift ├── Log.swift ├── MacTcode.entitlements ├── MacTcodeApp.swift ├── Mazegaki │ ├── Mazegaki.swift │ └── mazegaki.dic ├── Mode │ ├── Controller.swift │ ├── Mode.swift │ └── ModeWithCandidates.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Tcode │ ├── TcodeInputController.swift │ ├── TcodeKeymap.swift │ └── TcodeMode.swift ├── Zenkaku │ ├── ZenkakuMode.swift │ └── ZenkakuModeAction.swift ├── main.tiff ├── main@2x.tiff ├── mainw.tiff └── mainw@2x.tiff ├── MacTcodeTests ├── BushuTests.swift ├── ContextClientTests.swift ├── KeymapTests.swift ├── MazegakiSelectionTests.swift ├── MazegakiTests.swift ├── RecentTextTests.swift ├── TranslationTests.swift └── ZenkakuModeTest.swift ├── MacTcodeUITests ├── MacTcodeUITests.swift └── MacTcodeUITestsLaunchTests.swift ├── Makefile ├── README.md ├── logo.png ├── logo.xcf ├── mactcode-demo.gif ├── modeicon.xcf └── reload.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swiftmodule 3 | *.app 4 | xcuserdata/ 5 | *.xccheckout 6 | *.xcscmblueprint 7 | build/ 8 | DerivedData/ 9 | *.moved-aside 10 | *.pbxuser 11 | *.mode1v3 12 | *.mode2v3 13 | *.perspectivev3 14 | !default.pbxuser 15 | !default.mode1v3 16 | !default.mode2v3 17 | !default.perspectivev3 18 | *.xcodeproj/* 19 | !*.xcodeproj/project.pbxproj 20 | !*.xcodeproj/xcshareddata/ 21 | **/xcshareddata/WorkspaceSettings.xcsettings 22 | !*.xcworkspace/contents.xcworkspacedata 23 | work/ 24 | .vscode 25 | release/ 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2024 Kaoru Maeda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MacTcode.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 9D0F3CDF2C04D00F005A0CAC /* Bushu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0F3CDE2C04D00E005A0CAC /* Bushu.swift */; }; 11 | 9D0F3CE12C04D570005A0CAC /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0F3CE02C04D570005A0CAC /* Config.swift */; }; 12 | 9D0F3CE52C04DEAC005A0CAC /* BushuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0F3CE42C04DEAC005A0CAC /* BushuTests.swift */; }; 13 | 9D0F3CE72C04E139005A0CAC /* bushu.dic in Resources */ = {isa = PBXBuildFile; fileRef = 9D0F3CE62C04E139005A0CAC /* bushu.dic */; }; 14 | 9D4818DA2C0368DB00906504 /* main.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 9D4818D82C0368DB00906504 /* main.tiff */; }; 15 | 9D868A012C05F98700128D48 /* main@2x.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 9D868A002C05F98700128D48 /* main@2x.tiff */; }; 16 | 9D868FF32C706AB200293645 /* ContextClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D868FF22C706AB200293645 /* ContextClientTests.swift */; }; 17 | 9D8BFEC82C0FA9DA00B99BA2 /* ZenkakuModeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8BFEC72C0FA9DA00B99BA2 /* ZenkakuModeTest.swift */; }; 18 | 9D8BFECA2C146DF800B99BA2 /* MazegakiSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8BFEC92C146DF800B99BA2 /* MazegakiSelectionTests.swift */; }; 19 | 9D8BFECC2C15D64F00B99BA2 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8BFECB2C15D64F00B99BA2 /* Log.swift */; }; 20 | 9D8C8DB82C01ACEE003D8A43 /* MacTcodeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8C8DB72C01ACEE003D8A43 /* MacTcodeApp.swift */; }; 21 | 9D8C8DBF2C01ACF0003D8A43 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D8C8DBE2C01ACF0003D8A43 /* Preview Assets.xcassets */; }; 22 | 9D8C8DCA2C01ACF1003D8A43 /* KeymapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8C8DC92C01ACF1003D8A43 /* KeymapTests.swift */; }; 23 | 9D8C8DD42C01ACF1003D8A43 /* MacTcodeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8C8DD32C01ACF1003D8A43 /* MacTcodeUITests.swift */; }; 24 | 9D8C8DD62C01ACF1003D8A43 /* MacTcodeUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8C8DD52C01ACF1003D8A43 /* MacTcodeUITestsLaunchTests.swift */; }; 25 | 9D8C8DE52C0338A7003D8A43 /* TcodeInputController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8C8DE42C0338A7003D8A43 /* TcodeInputController.swift */; }; 26 | 9D8C8DE72C0338EB003D8A43 /* TcodeKeymap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8C8DE62C0338EB003D8A43 /* TcodeKeymap.swift */; }; 27 | 9D8C8DE92C035BBA003D8A43 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D8C8DE82C035BBA003D8A43 /* Assets.xcassets */; }; 28 | 9DC8D4962C061E5F00DCB6A2 /* Mazegaki.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DC8D4952C061E5F00DCB6A2 /* Mazegaki.swift */; }; 29 | 9DC8D4982C061F3800DCB6A2 /* mazegaki.dic in Resources */ = {isa = PBXBuildFile; fileRef = 9DC8D4972C061F3800DCB6A2 /* mazegaki.dic */; }; 30 | 9DC8D49A2C0632C900DCB6A2 /* MazegakiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DC8D4992C0632C900DCB6A2 /* MazegakiTests.swift */; }; 31 | 9DDFC1662C09982F00BAADB5 /* mainw.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 9DDFC1642C09982F00BAADB5 /* mainw.tiff */; }; 32 | 9DDFC1672C09982F00BAADB5 /* mainw@2x.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 9DDFC1652C09982F00BAADB5 /* mainw@2x.tiff */; }; 33 | 9DEA6D382C0AC0DA0005BB18 /* Keymap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEA6D372C0AC0DA0005BB18 /* Keymap.swift */; }; 34 | 9DEA6D3A2C0AC7F90005BB18 /* Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEA6D392C0AC7F90005BB18 /* Action.swift */; }; 35 | 9DEA6D3E2C0B40320005BB18 /* RecentTextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEA6D3D2C0B40320005BB18 /* RecentTextTests.swift */; }; 36 | 9DEA6D402C0B6C680005BB18 /* TranslationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEA6D3F2C0B6C680005BB18 /* TranslationTests.swift */; }; 37 | 9DEA6D422C0B94360005BB18 /* RecentTextClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEA6D412C0B94360005BB18 /* RecentTextClient.swift */; }; 38 | 9DEA6D442C0C0FD10005BB18 /* InputEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEA6D432C0C0FD10005BB18 /* InputEvent.swift */; }; 39 | 9DEA6D462C0E45950005BB18 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEA6D452C0E45950005BB18 /* Client.swift */; }; 40 | 9DEA6D482C0EB0430005BB18 /* Mode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEA6D472C0EB0430005BB18 /* Mode.swift */; }; 41 | 9DEA6D4A2C0F987B0005BB18 /* ZenkakuMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEA6D492C0F987B0005BB18 /* ZenkakuMode.swift */; }; 42 | 9DEAFE162C5F9B3000E8B5E3 /* EmitPendingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE152C5F9B3000E8B5E3 /* EmitPendingAction.swift */; }; 43 | 9DEAFE182C5F9B5400E8B5E3 /* RemoveLastPendingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE172C5F9B5400E8B5E3 /* RemoveLastPendingAction.swift */; }; 44 | 9DEAFE1A2C5F9B7900E8B5E3 /* ResetAllStateAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE192C5F9B7900E8B5E3 /* ResetAllStateAction.swift */; }; 45 | 9DEAFE1F2C5F9E5F00E8B5E3 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE1E2C5F9E5F00E8B5E3 /* Command.swift */; }; 46 | 9DEAFE212C5F9E8600E8B5E3 /* KeymapResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE202C5F9E8600E8B5E3 /* KeymapResolver.swift */; }; 47 | 9DEAFE232C5F9EE300E8B5E3 /* Translator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE222C5F9EE300E8B5E3 /* Translator.swift */; }; 48 | 9DEAFE252C5FA25D00E8B5E3 /* MultiStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE242C5FA25D00E8B5E3 /* MultiStroke.swift */; }; 49 | 9DEAFE272C5FA2A800E8B5E3 /* Controller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE262C5FA2A800E8B5E3 /* Controller.swift */; }; 50 | 9DEAFE292C5FA2CF00E8B5E3 /* ModeWithCandidates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE282C5FA2CF00E8B5E3 /* ModeWithCandidates.swift */; }; 51 | 9DEAFE2B2C5FB18700E8B5E3 /* ContextClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE2A2C5FB18700E8B5E3 /* ContextClient.swift */; }; 52 | 9DEAFE2D2C5FB24500E8B5E3 /* ZenkakuModeAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE2C2C5FB24500E8B5E3 /* ZenkakuModeAction.swift */; }; 53 | 9DEAFE332C5FD35D00E8B5E3 /* TcodeMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE322C5FD35D00E8B5E3 /* TcodeMode.swift */; }; 54 | 9DEAFE352C6DB3E000E8B5E3 /* ClientWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE342C6DB3E000E8B5E3 /* ClientWrapper.swift */; }; 55 | 9DEAFE372C6DB8CF00E8B5E3 /* YomiContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEAFE362C6DB8CF00E8B5E3 /* YomiContext.swift */; }; 56 | /* End PBXBuildFile section */ 57 | 58 | /* Begin PBXContainerItemProxy section */ 59 | 9D8C8DC62C01ACF1003D8A43 /* PBXContainerItemProxy */ = { 60 | isa = PBXContainerItemProxy; 61 | containerPortal = 9D8C8DAC2C01ACEE003D8A43 /* Project object */; 62 | proxyType = 1; 63 | remoteGlobalIDString = 9D8C8DB32C01ACEE003D8A43; 64 | remoteInfo = MacTcode; 65 | }; 66 | 9D8C8DD02C01ACF1003D8A43 /* PBXContainerItemProxy */ = { 67 | isa = PBXContainerItemProxy; 68 | containerPortal = 9D8C8DAC2C01ACEE003D8A43 /* Project object */; 69 | proxyType = 1; 70 | remoteGlobalIDString = 9D8C8DB32C01ACEE003D8A43; 71 | remoteInfo = MacTcode; 72 | }; 73 | /* End PBXContainerItemProxy section */ 74 | 75 | /* Begin PBXFileReference section */ 76 | 9D0F3CDE2C04D00E005A0CAC /* Bushu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bushu.swift; sourceTree = ""; }; 77 | 9D0F3CE02C04D570005A0CAC /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 78 | 9D0F3CE42C04DEAC005A0CAC /* BushuTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BushuTests.swift; sourceTree = ""; }; 79 | 9D0F3CE62C04E139005A0CAC /* bushu.dic */ = {isa = PBXFileReference; explicitFileType = text; fileEncoding = 4; path = bushu.dic; sourceTree = ""; }; 80 | 9D4818D82C0368DB00906504 /* main.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = main.tiff; sourceTree = ""; }; 81 | 9D6BA6A82C462793007524AC /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = System/Library/Frameworks/InputMethodKit.framework; sourceTree = SDKROOT; }; 82 | 9D6BA6AA2C46279A007524AC /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 83 | 9D868A002C05F98700128D48 /* main@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "main@2x.tiff"; sourceTree = ""; }; 84 | 9D868FF22C706AB200293645 /* ContextClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextClientTests.swift; sourceTree = ""; }; 85 | 9D8BFEC72C0FA9DA00B99BA2 /* ZenkakuModeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZenkakuModeTest.swift; sourceTree = ""; }; 86 | 9D8BFEC92C146DF800B99BA2 /* MazegakiSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MazegakiSelectionTests.swift; sourceTree = ""; }; 87 | 9D8BFECB2C15D64F00B99BA2 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = ""; }; 88 | 9D8C8DB42C01ACEE003D8A43 /* MacTcode.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTcode.app; sourceTree = BUILT_PRODUCTS_DIR; }; 89 | 9D8C8DB72C01ACEE003D8A43 /* MacTcodeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTcodeApp.swift; sourceTree = ""; }; 90 | 9D8C8DBE2C01ACF0003D8A43 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 91 | 9D8C8DC02C01ACF0003D8A43 /* MacTcode.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MacTcode.entitlements; sourceTree = ""; }; 92 | 9D8C8DC52C01ACF1003D8A43 /* MacTcodeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MacTcodeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 93 | 9D8C8DC92C01ACF1003D8A43 /* KeymapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymapTests.swift; sourceTree = ""; }; 94 | 9D8C8DCF2C01ACF1003D8A43 /* MacTcodeUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MacTcodeUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 95 | 9D8C8DD32C01ACF1003D8A43 /* MacTcodeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTcodeUITests.swift; sourceTree = ""; }; 96 | 9D8C8DD52C01ACF1003D8A43 /* MacTcodeUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTcodeUITestsLaunchTests.swift; sourceTree = ""; }; 97 | 9D8C8DE22C01ADA9003D8A43 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 98 | 9D8C8DE42C0338A7003D8A43 /* TcodeInputController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TcodeInputController.swift; sourceTree = ""; }; 99 | 9D8C8DE62C0338EB003D8A43 /* TcodeKeymap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TcodeKeymap.swift; sourceTree = ""; }; 100 | 9D8C8DE82C035BBA003D8A43 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 101 | 9DC8D4952C061E5F00DCB6A2 /* Mazegaki.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mazegaki.swift; sourceTree = ""; }; 102 | 9DC8D4972C061F3800DCB6A2 /* mazegaki.dic */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = mazegaki.dic; sourceTree = ""; }; 103 | 9DC8D4992C0632C900DCB6A2 /* MazegakiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MazegakiTests.swift; sourceTree = ""; }; 104 | 9DDFC1642C09982F00BAADB5 /* mainw.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = mainw.tiff; sourceTree = ""; }; 105 | 9DDFC1652C09982F00BAADB5 /* mainw@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "mainw@2x.tiff"; sourceTree = ""; }; 106 | 9DEA6D372C0AC0DA0005BB18 /* Keymap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keymap.swift; sourceTree = ""; }; 107 | 9DEA6D392C0AC7F90005BB18 /* Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Action.swift; sourceTree = ""; }; 108 | 9DEA6D3D2C0B40320005BB18 /* RecentTextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentTextTests.swift; sourceTree = ""; }; 109 | 9DEA6D3F2C0B6C680005BB18 /* TranslationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationTests.swift; sourceTree = ""; }; 110 | 9DEA6D412C0B94360005BB18 /* RecentTextClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentTextClient.swift; sourceTree = ""; }; 111 | 9DEA6D432C0C0FD10005BB18 /* InputEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputEvent.swift; sourceTree = ""; }; 112 | 9DEA6D452C0E45950005BB18 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = ""; }; 113 | 9DEA6D472C0EB0430005BB18 /* Mode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mode.swift; sourceTree = ""; }; 114 | 9DEA6D492C0F987B0005BB18 /* ZenkakuMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZenkakuMode.swift; sourceTree = ""; }; 115 | 9DEAFE152C5F9B3000E8B5E3 /* EmitPendingAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmitPendingAction.swift; sourceTree = ""; }; 116 | 9DEAFE172C5F9B5400E8B5E3 /* RemoveLastPendingAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveLastPendingAction.swift; sourceTree = ""; }; 117 | 9DEAFE192C5F9B7900E8B5E3 /* ResetAllStateAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetAllStateAction.swift; sourceTree = ""; }; 118 | 9DEAFE1E2C5F9E5F00E8B5E3 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; 119 | 9DEAFE202C5F9E8600E8B5E3 /* KeymapResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeymapResolver.swift; sourceTree = ""; }; 120 | 9DEAFE222C5F9EE300E8B5E3 /* Translator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translator.swift; sourceTree = ""; }; 121 | 9DEAFE242C5FA25D00E8B5E3 /* MultiStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiStroke.swift; sourceTree = ""; }; 122 | 9DEAFE262C5FA2A800E8B5E3 /* Controller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Controller.swift; sourceTree = ""; }; 123 | 9DEAFE282C5FA2CF00E8B5E3 /* ModeWithCandidates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModeWithCandidates.swift; sourceTree = ""; }; 124 | 9DEAFE2A2C5FB18700E8B5E3 /* ContextClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextClient.swift; sourceTree = ""; }; 125 | 9DEAFE2C2C5FB24500E8B5E3 /* ZenkakuModeAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZenkakuModeAction.swift; sourceTree = ""; }; 126 | 9DEAFE322C5FD35D00E8B5E3 /* TcodeMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TcodeMode.swift; sourceTree = ""; }; 127 | 9DEAFE342C6DB3E000E8B5E3 /* ClientWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientWrapper.swift; sourceTree = ""; }; 128 | 9DEAFE362C6DB8CF00E8B5E3 /* YomiContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YomiContext.swift; sourceTree = ""; }; 129 | /* End PBXFileReference section */ 130 | 131 | /* Begin PBXFrameworksBuildPhase section */ 132 | 9D8C8DB12C01ACEE003D8A43 /* Frameworks */ = { 133 | isa = PBXFrameworksBuildPhase; 134 | buildActionMask = 2147483647; 135 | files = ( 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | 9D8C8DC22C01ACF1003D8A43 /* Frameworks */ = { 140 | isa = PBXFrameworksBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | 9D8C8DCC2C01ACF1003D8A43 /* Frameworks */ = { 147 | isa = PBXFrameworksBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | /* End PBXFrameworksBuildPhase section */ 154 | 155 | /* Begin PBXGroup section */ 156 | 9D6BA6A72C462793007524AC /* Frameworks */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 9D6BA6AA2C46279A007524AC /* Cocoa.framework */, 160 | 9D6BA6A82C462793007524AC /* InputMethodKit.framework */, 161 | ); 162 | name = Frameworks; 163 | sourceTree = ""; 164 | }; 165 | 9D8C8DAB2C01ACEE003D8A43 = { 166 | isa = PBXGroup; 167 | children = ( 168 | 9D8C8DB62C01ACEE003D8A43 /* MacTcode */, 169 | 9D8C8DC82C01ACF1003D8A43 /* MacTcodeTests */, 170 | 9D8C8DD22C01ACF1003D8A43 /* MacTcodeUITests */, 171 | 9D8C8DB52C01ACEE003D8A43 /* Products */, 172 | 9D6BA6A72C462793007524AC /* Frameworks */, 173 | ); 174 | sourceTree = ""; 175 | }; 176 | 9D8C8DB52C01ACEE003D8A43 /* Products */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | 9D8C8DB42C01ACEE003D8A43 /* MacTcode.app */, 180 | 9D8C8DC52C01ACF1003D8A43 /* MacTcodeTests.xctest */, 181 | 9D8C8DCF2C01ACF1003D8A43 /* MacTcodeUITests.xctest */, 182 | ); 183 | name = Products; 184 | sourceTree = ""; 185 | }; 186 | 9D8C8DB62C01ACEE003D8A43 /* MacTcode */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | 9D8C8DC02C01ACF0003D8A43 /* MacTcode.entitlements */, 190 | 9D8C8DE22C01ADA9003D8A43 /* Info.plist */, 191 | 9DEA6D392C0AC7F90005BB18 /* Action.swift */, 192 | 9DEAFE1E2C5F9E5F00E8B5E3 /* Command.swift */, 193 | 9D0F3CE02C04D570005A0CAC /* Config.swift */, 194 | 9D8BFECB2C15D64F00B99BA2 /* Log.swift */, 195 | 9D8C8DB72C01ACEE003D8A43 /* MacTcodeApp.swift */, 196 | 9DEAFE312C5FB30400E8B5E3 /* Zenkaku */, 197 | 9D4818D82C0368DB00906504 /* main.tiff */, 198 | 9D868A002C05F98700128D48 /* main@2x.tiff */, 199 | 9DDFC1642C09982F00BAADB5 /* mainw.tiff */, 200 | 9DDFC1652C09982F00BAADB5 /* mainw@2x.tiff */, 201 | 9D8C8DE82C035BBA003D8A43 /* Assets.xcassets */, 202 | 9DEAFE1B2C5F9D1B00E8B5E3 /* Bushu */, 203 | 9DEAFE2F2C5FB2D200E8B5E3 /* Client */, 204 | 9DEAFE2E2C5FB2B300E8B5E3 /* Keymap */, 205 | 9DEAFE1C2C5F9D4C00E8B5E3 /* Mazegaki */, 206 | 9DEAFE302C5FB2EC00E8B5E3 /* Mode */, 207 | 9D8C8DBD2C01ACF0003D8A43 /* Preview Content */, 208 | 9DEAFE1D2C5F9DC300E8B5E3 /* Tcode */, 209 | ); 210 | path = MacTcode; 211 | sourceTree = ""; 212 | }; 213 | 9D8C8DBD2C01ACF0003D8A43 /* Preview Content */ = { 214 | isa = PBXGroup; 215 | children = ( 216 | 9D8C8DBE2C01ACF0003D8A43 /* Preview Assets.xcassets */, 217 | ); 218 | path = "Preview Content"; 219 | sourceTree = ""; 220 | }; 221 | 9D8C8DC82C01ACF1003D8A43 /* MacTcodeTests */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | 9D8C8DC92C01ACF1003D8A43 /* KeymapTests.swift */, 225 | 9D0F3CE42C04DEAC005A0CAC /* BushuTests.swift */, 226 | 9DC8D4992C0632C900DCB6A2 /* MazegakiTests.swift */, 227 | 9DEA6D3D2C0B40320005BB18 /* RecentTextTests.swift */, 228 | 9DEA6D3F2C0B6C680005BB18 /* TranslationTests.swift */, 229 | 9D8BFEC72C0FA9DA00B99BA2 /* ZenkakuModeTest.swift */, 230 | 9D8BFEC92C146DF800B99BA2 /* MazegakiSelectionTests.swift */, 231 | 9D868FF22C706AB200293645 /* ContextClientTests.swift */, 232 | ); 233 | path = MacTcodeTests; 234 | sourceTree = ""; 235 | }; 236 | 9D8C8DD22C01ACF1003D8A43 /* MacTcodeUITests */ = { 237 | isa = PBXGroup; 238 | children = ( 239 | 9D8C8DD32C01ACF1003D8A43 /* MacTcodeUITests.swift */, 240 | 9D8C8DD52C01ACF1003D8A43 /* MacTcodeUITestsLaunchTests.swift */, 241 | ); 242 | path = MacTcodeUITests; 243 | sourceTree = ""; 244 | }; 245 | 9DEAFE1B2C5F9D1B00E8B5E3 /* Bushu */ = { 246 | isa = PBXGroup; 247 | children = ( 248 | 9D0F3CE62C04E139005A0CAC /* bushu.dic */, 249 | 9D0F3CDE2C04D00E005A0CAC /* Bushu.swift */, 250 | ); 251 | path = Bushu; 252 | sourceTree = ""; 253 | }; 254 | 9DEAFE1C2C5F9D4C00E8B5E3 /* Mazegaki */ = { 255 | isa = PBXGroup; 256 | children = ( 257 | 9DC8D4972C061F3800DCB6A2 /* mazegaki.dic */, 258 | 9DC8D4952C061E5F00DCB6A2 /* Mazegaki.swift */, 259 | ); 260 | path = Mazegaki; 261 | sourceTree = ""; 262 | }; 263 | 9DEAFE1D2C5F9DC300E8B5E3 /* Tcode */ = { 264 | isa = PBXGroup; 265 | children = ( 266 | 9D8C8DE42C0338A7003D8A43 /* TcodeInputController.swift */, 267 | 9DEAFE322C5FD35D00E8B5E3 /* TcodeMode.swift */, 268 | 9D8C8DE62C0338EB003D8A43 /* TcodeKeymap.swift */, 269 | ); 270 | path = Tcode; 271 | sourceTree = ""; 272 | }; 273 | 9DEAFE2E2C5FB2B300E8B5E3 /* Keymap */ = { 274 | isa = PBXGroup; 275 | children = ( 276 | 9DEAFE152C5F9B3000E8B5E3 /* EmitPendingAction.swift */, 277 | 9DEA6D432C0C0FD10005BB18 /* InputEvent.swift */, 278 | 9DEA6D372C0AC0DA0005BB18 /* Keymap.swift */, 279 | 9DEAFE202C5F9E8600E8B5E3 /* KeymapResolver.swift */, 280 | 9DEAFE242C5FA25D00E8B5E3 /* MultiStroke.swift */, 281 | 9DEAFE172C5F9B5400E8B5E3 /* RemoveLastPendingAction.swift */, 282 | 9DEAFE192C5F9B7900E8B5E3 /* ResetAllStateAction.swift */, 283 | 9DEAFE222C5F9EE300E8B5E3 /* Translator.swift */, 284 | ); 285 | path = Keymap; 286 | sourceTree = ""; 287 | }; 288 | 9DEAFE2F2C5FB2D200E8B5E3 /* Client */ = { 289 | isa = PBXGroup; 290 | children = ( 291 | 9DEA6D452C0E45950005BB18 /* Client.swift */, 292 | 9DEAFE2A2C5FB18700E8B5E3 /* ContextClient.swift */, 293 | 9DEA6D412C0B94360005BB18 /* RecentTextClient.swift */, 294 | 9DEAFE342C6DB3E000E8B5E3 /* ClientWrapper.swift */, 295 | 9DEAFE362C6DB8CF00E8B5E3 /* YomiContext.swift */, 296 | ); 297 | path = Client; 298 | sourceTree = ""; 299 | }; 300 | 9DEAFE302C5FB2EC00E8B5E3 /* Mode */ = { 301 | isa = PBXGroup; 302 | children = ( 303 | 9DEAFE262C5FA2A800E8B5E3 /* Controller.swift */, 304 | 9DEA6D472C0EB0430005BB18 /* Mode.swift */, 305 | 9DEAFE282C5FA2CF00E8B5E3 /* ModeWithCandidates.swift */, 306 | ); 307 | path = Mode; 308 | sourceTree = ""; 309 | }; 310 | 9DEAFE312C5FB30400E8B5E3 /* Zenkaku */ = { 311 | isa = PBXGroup; 312 | children = ( 313 | 9DEA6D492C0F987B0005BB18 /* ZenkakuMode.swift */, 314 | 9DEAFE2C2C5FB24500E8B5E3 /* ZenkakuModeAction.swift */, 315 | ); 316 | path = Zenkaku; 317 | sourceTree = ""; 318 | }; 319 | /* End PBXGroup section */ 320 | 321 | /* Begin PBXNativeTarget section */ 322 | 9D8C8DB32C01ACEE003D8A43 /* MacTcode */ = { 323 | isa = PBXNativeTarget; 324 | buildConfigurationList = 9D8C8DD92C01ACF1003D8A43 /* Build configuration list for PBXNativeTarget "MacTcode" */; 325 | buildPhases = ( 326 | 9D8C8DB02C01ACEE003D8A43 /* Sources */, 327 | 9D8C8DB12C01ACEE003D8A43 /* Frameworks */, 328 | 9D8C8DB22C01ACEE003D8A43 /* Resources */, 329 | ); 330 | buildRules = ( 331 | ); 332 | dependencies = ( 333 | ); 334 | name = MacTcode; 335 | productName = MacTcode; 336 | productReference = 9D8C8DB42C01ACEE003D8A43 /* MacTcode.app */; 337 | productType = "com.apple.product-type.application"; 338 | }; 339 | 9D8C8DC42C01ACF1003D8A43 /* MacTcodeTests */ = { 340 | isa = PBXNativeTarget; 341 | buildConfigurationList = 9D8C8DDC2C01ACF1003D8A43 /* Build configuration list for PBXNativeTarget "MacTcodeTests" */; 342 | buildPhases = ( 343 | 9D8C8DC12C01ACF1003D8A43 /* Sources */, 344 | 9D8C8DC22C01ACF1003D8A43 /* Frameworks */, 345 | 9D8C8DC32C01ACF1003D8A43 /* Resources */, 346 | ); 347 | buildRules = ( 348 | ); 349 | dependencies = ( 350 | 9D8C8DC72C01ACF1003D8A43 /* PBXTargetDependency */, 351 | ); 352 | name = MacTcodeTests; 353 | productName = MacTcodeTests; 354 | productReference = 9D8C8DC52C01ACF1003D8A43 /* MacTcodeTests.xctest */; 355 | productType = "com.apple.product-type.bundle.unit-test"; 356 | }; 357 | 9D8C8DCE2C01ACF1003D8A43 /* MacTcodeUITests */ = { 358 | isa = PBXNativeTarget; 359 | buildConfigurationList = 9D8C8DDF2C01ACF1003D8A43 /* Build configuration list for PBXNativeTarget "MacTcodeUITests" */; 360 | buildPhases = ( 361 | 9D8C8DCB2C01ACF1003D8A43 /* Sources */, 362 | 9D8C8DCC2C01ACF1003D8A43 /* Frameworks */, 363 | 9D8C8DCD2C01ACF1003D8A43 /* Resources */, 364 | ); 365 | buildRules = ( 366 | ); 367 | dependencies = ( 368 | 9D8C8DD12C01ACF1003D8A43 /* PBXTargetDependency */, 369 | ); 370 | name = MacTcodeUITests; 371 | productName = MacTcodeUITests; 372 | productReference = 9D8C8DCF2C01ACF1003D8A43 /* MacTcodeUITests.xctest */; 373 | productType = "com.apple.product-type.bundle.ui-testing"; 374 | }; 375 | /* End PBXNativeTarget section */ 376 | 377 | /* Begin PBXProject section */ 378 | 9D8C8DAC2C01ACEE003D8A43 /* Project object */ = { 379 | isa = PBXProject; 380 | attributes = { 381 | BuildIndependentTargetsInParallel = 1; 382 | LastSwiftUpdateCheck = 1540; 383 | LastUpgradeCheck = 1620; 384 | TargetAttributes = { 385 | 9D8C8DB32C01ACEE003D8A43 = { 386 | CreatedOnToolsVersion = 15.4; 387 | }; 388 | 9D8C8DC42C01ACF1003D8A43 = { 389 | CreatedOnToolsVersion = 15.4; 390 | TestTargetID = 9D8C8DB32C01ACEE003D8A43; 391 | }; 392 | 9D8C8DCE2C01ACF1003D8A43 = { 393 | CreatedOnToolsVersion = 15.4; 394 | TestTargetID = 9D8C8DB32C01ACEE003D8A43; 395 | }; 396 | }; 397 | }; 398 | buildConfigurationList = 9D8C8DAF2C01ACEE003D8A43 /* Build configuration list for PBXProject "MacTcode" */; 399 | compatibilityVersion = "Xcode 13.0"; 400 | developmentRegion = en; 401 | hasScannedForEncodings = 0; 402 | knownRegions = ( 403 | en, 404 | Base, 405 | ); 406 | mainGroup = 9D8C8DAB2C01ACEE003D8A43; 407 | productRefGroup = 9D8C8DB52C01ACEE003D8A43 /* Products */; 408 | projectDirPath = ""; 409 | projectRoot = ""; 410 | targets = ( 411 | 9D8C8DB32C01ACEE003D8A43 /* MacTcode */, 412 | 9D8C8DC42C01ACF1003D8A43 /* MacTcodeTests */, 413 | 9D8C8DCE2C01ACF1003D8A43 /* MacTcodeUITests */, 414 | ); 415 | }; 416 | /* End PBXProject section */ 417 | 418 | /* Begin PBXResourcesBuildPhase section */ 419 | 9D8C8DB22C01ACEE003D8A43 /* Resources */ = { 420 | isa = PBXResourcesBuildPhase; 421 | buildActionMask = 2147483647; 422 | files = ( 423 | 9D8C8DBF2C01ACF0003D8A43 /* Preview Assets.xcassets in Resources */, 424 | 9DDFC1662C09982F00BAADB5 /* mainw.tiff in Resources */, 425 | 9DDFC1672C09982F00BAADB5 /* mainw@2x.tiff in Resources */, 426 | 9DC8D4982C061F3800DCB6A2 /* mazegaki.dic in Resources */, 427 | 9D868A012C05F98700128D48 /* main@2x.tiff in Resources */, 428 | 9D4818DA2C0368DB00906504 /* main.tiff in Resources */, 429 | 9D0F3CE72C04E139005A0CAC /* bushu.dic in Resources */, 430 | 9D8C8DE92C035BBA003D8A43 /* Assets.xcassets in Resources */, 431 | ); 432 | runOnlyForDeploymentPostprocessing = 0; 433 | }; 434 | 9D8C8DC32C01ACF1003D8A43 /* Resources */ = { 435 | isa = PBXResourcesBuildPhase; 436 | buildActionMask = 2147483647; 437 | files = ( 438 | ); 439 | runOnlyForDeploymentPostprocessing = 0; 440 | }; 441 | 9D8C8DCD2C01ACF1003D8A43 /* Resources */ = { 442 | isa = PBXResourcesBuildPhase; 443 | buildActionMask = 2147483647; 444 | files = ( 445 | ); 446 | runOnlyForDeploymentPostprocessing = 0; 447 | }; 448 | /* End PBXResourcesBuildPhase section */ 449 | 450 | /* Begin PBXSourcesBuildPhase section */ 451 | 9D8C8DB02C01ACEE003D8A43 /* Sources */ = { 452 | isa = PBXSourcesBuildPhase; 453 | buildActionMask = 2147483647; 454 | files = ( 455 | 9DEAFE372C6DB8CF00E8B5E3 /* YomiContext.swift in Sources */, 456 | 9DC8D4962C061E5F00DCB6A2 /* Mazegaki.swift in Sources */, 457 | 9DEAFE292C5FA2CF00E8B5E3 /* ModeWithCandidates.swift in Sources */, 458 | 9DEA6D422C0B94360005BB18 /* RecentTextClient.swift in Sources */, 459 | 9DEAFE162C5F9B3000E8B5E3 /* EmitPendingAction.swift in Sources */, 460 | 9DEAFE332C5FD35D00E8B5E3 /* TcodeMode.swift in Sources */, 461 | 9DEAFE212C5F9E8600E8B5E3 /* KeymapResolver.swift in Sources */, 462 | 9DEA6D482C0EB0430005BB18 /* Mode.swift in Sources */, 463 | 9DEAFE1A2C5F9B7900E8B5E3 /* ResetAllStateAction.swift in Sources */, 464 | 9DEAFE1F2C5F9E5F00E8B5E3 /* Command.swift in Sources */, 465 | 9DEAFE2B2C5FB18700E8B5E3 /* ContextClient.swift in Sources */, 466 | 9DEA6D442C0C0FD10005BB18 /* InputEvent.swift in Sources */, 467 | 9DEAFE2D2C5FB24500E8B5E3 /* ZenkakuModeAction.swift in Sources */, 468 | 9D8C8DB82C01ACEE003D8A43 /* MacTcodeApp.swift in Sources */, 469 | 9DEA6D462C0E45950005BB18 /* Client.swift in Sources */, 470 | 9D0F3CDF2C04D00F005A0CAC /* Bushu.swift in Sources */, 471 | 9DEAFE352C6DB3E000E8B5E3 /* ClientWrapper.swift in Sources */, 472 | 9D0F3CE12C04D570005A0CAC /* Config.swift in Sources */, 473 | 9D8BFECC2C15D64F00B99BA2 /* Log.swift in Sources */, 474 | 9DEAFE272C5FA2A800E8B5E3 /* Controller.swift in Sources */, 475 | 9DEA6D382C0AC0DA0005BB18 /* Keymap.swift in Sources */, 476 | 9DEA6D3A2C0AC7F90005BB18 /* Action.swift in Sources */, 477 | 9D8C8DE72C0338EB003D8A43 /* TcodeKeymap.swift in Sources */, 478 | 9DEAFE252C5FA25D00E8B5E3 /* MultiStroke.swift in Sources */, 479 | 9DEAFE182C5F9B5400E8B5E3 /* RemoveLastPendingAction.swift in Sources */, 480 | 9DEAFE232C5F9EE300E8B5E3 /* Translator.swift in Sources */, 481 | 9DEA6D4A2C0F987B0005BB18 /* ZenkakuMode.swift in Sources */, 482 | 9D8C8DE52C0338A7003D8A43 /* TcodeInputController.swift in Sources */, 483 | ); 484 | runOnlyForDeploymentPostprocessing = 0; 485 | }; 486 | 9D8C8DC12C01ACF1003D8A43 /* Sources */ = { 487 | isa = PBXSourcesBuildPhase; 488 | buildActionMask = 2147483647; 489 | files = ( 490 | 9D8C8DCA2C01ACF1003D8A43 /* KeymapTests.swift in Sources */, 491 | 9DEA6D3E2C0B40320005BB18 /* RecentTextTests.swift in Sources */, 492 | 9D868FF32C706AB200293645 /* ContextClientTests.swift in Sources */, 493 | 9DEA6D402C0B6C680005BB18 /* TranslationTests.swift in Sources */, 494 | 9DC8D49A2C0632C900DCB6A2 /* MazegakiTests.swift in Sources */, 495 | 9D8BFEC82C0FA9DA00B99BA2 /* ZenkakuModeTest.swift in Sources */, 496 | 9D0F3CE52C04DEAC005A0CAC /* BushuTests.swift in Sources */, 497 | 9D8BFECA2C146DF800B99BA2 /* MazegakiSelectionTests.swift in Sources */, 498 | ); 499 | runOnlyForDeploymentPostprocessing = 0; 500 | }; 501 | 9D8C8DCB2C01ACF1003D8A43 /* Sources */ = { 502 | isa = PBXSourcesBuildPhase; 503 | buildActionMask = 2147483647; 504 | files = ( 505 | 9D8C8DD62C01ACF1003D8A43 /* MacTcodeUITestsLaunchTests.swift in Sources */, 506 | 9D8C8DD42C01ACF1003D8A43 /* MacTcodeUITests.swift in Sources */, 507 | ); 508 | runOnlyForDeploymentPostprocessing = 0; 509 | }; 510 | /* End PBXSourcesBuildPhase section */ 511 | 512 | /* Begin PBXTargetDependency section */ 513 | 9D8C8DC72C01ACF1003D8A43 /* PBXTargetDependency */ = { 514 | isa = PBXTargetDependency; 515 | target = 9D8C8DB32C01ACEE003D8A43 /* MacTcode */; 516 | targetProxy = 9D8C8DC62C01ACF1003D8A43 /* PBXContainerItemProxy */; 517 | }; 518 | 9D8C8DD12C01ACF1003D8A43 /* PBXTargetDependency */ = { 519 | isa = PBXTargetDependency; 520 | target = 9D8C8DB32C01ACEE003D8A43 /* MacTcode */; 521 | targetProxy = 9D8C8DD02C01ACF1003D8A43 /* PBXContainerItemProxy */; 522 | }; 523 | /* End PBXTargetDependency section */ 524 | 525 | /* Begin XCBuildConfiguration section */ 526 | 9D8C8DD72C01ACF1003D8A43 /* Debug */ = { 527 | isa = XCBuildConfiguration; 528 | buildSettings = { 529 | ALWAYS_SEARCH_USER_PATHS = NO; 530 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 531 | CLANG_ANALYZER_NONNULL = YES; 532 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 533 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 534 | CLANG_ENABLE_MODULES = YES; 535 | CLANG_ENABLE_OBJC_ARC = YES; 536 | CLANG_ENABLE_OBJC_WEAK = YES; 537 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 538 | CLANG_WARN_BOOL_CONVERSION = YES; 539 | CLANG_WARN_COMMA = YES; 540 | CLANG_WARN_CONSTANT_CONVERSION = YES; 541 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 542 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 543 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 544 | CLANG_WARN_EMPTY_BODY = YES; 545 | CLANG_WARN_ENUM_CONVERSION = YES; 546 | CLANG_WARN_INFINITE_RECURSION = YES; 547 | CLANG_WARN_INT_CONVERSION = YES; 548 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 549 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 550 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 551 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 552 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 553 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 554 | CLANG_WARN_STRICT_PROTOTYPES = YES; 555 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 556 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 557 | CLANG_WARN_UNREACHABLE_CODE = YES; 558 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 559 | COPY_PHASE_STRIP = NO; 560 | DEAD_CODE_STRIPPING = YES; 561 | DEBUG_INFORMATION_FORMAT = dwarf; 562 | ENABLE_STRICT_OBJC_MSGSEND = YES; 563 | ENABLE_TESTABILITY = YES; 564 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 565 | GCC_C_LANGUAGE_STANDARD = gnu17; 566 | GCC_DYNAMIC_NO_PIC = NO; 567 | GCC_NO_COMMON_BLOCKS = YES; 568 | GCC_OPTIMIZATION_LEVEL = 0; 569 | GCC_PREPROCESSOR_DEFINITIONS = ( 570 | "DEBUG=1", 571 | "$(inherited)", 572 | ); 573 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 574 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 575 | GCC_WARN_UNDECLARED_SELECTOR = YES; 576 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 577 | GCC_WARN_UNUSED_FUNCTION = YES; 578 | GCC_WARN_UNUSED_VARIABLE = YES; 579 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 580 | MACOSX_DEPLOYMENT_TARGET = 14.5; 581 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 582 | MTL_FAST_MATH = YES; 583 | ONLY_ACTIVE_ARCH = YES; 584 | SDKROOT = macosx; 585 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 586 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 587 | }; 588 | name = Debug; 589 | }; 590 | 9D8C8DD82C01ACF1003D8A43 /* Release */ = { 591 | isa = XCBuildConfiguration; 592 | buildSettings = { 593 | ALWAYS_SEARCH_USER_PATHS = NO; 594 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 595 | CLANG_ANALYZER_NONNULL = YES; 596 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 597 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 598 | CLANG_ENABLE_MODULES = YES; 599 | CLANG_ENABLE_OBJC_ARC = YES; 600 | CLANG_ENABLE_OBJC_WEAK = YES; 601 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 602 | CLANG_WARN_BOOL_CONVERSION = YES; 603 | CLANG_WARN_COMMA = YES; 604 | CLANG_WARN_CONSTANT_CONVERSION = YES; 605 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 606 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 607 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 608 | CLANG_WARN_EMPTY_BODY = YES; 609 | CLANG_WARN_ENUM_CONVERSION = YES; 610 | CLANG_WARN_INFINITE_RECURSION = YES; 611 | CLANG_WARN_INT_CONVERSION = YES; 612 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 613 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 614 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 615 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 616 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 617 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 618 | CLANG_WARN_STRICT_PROTOTYPES = YES; 619 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 620 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 621 | CLANG_WARN_UNREACHABLE_CODE = YES; 622 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 623 | COPY_PHASE_STRIP = NO; 624 | DEAD_CODE_STRIPPING = YES; 625 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 626 | ENABLE_NS_ASSERTIONS = NO; 627 | ENABLE_STRICT_OBJC_MSGSEND = YES; 628 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 629 | GCC_C_LANGUAGE_STANDARD = gnu17; 630 | GCC_NO_COMMON_BLOCKS = YES; 631 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 632 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 633 | GCC_WARN_UNDECLARED_SELECTOR = YES; 634 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 635 | GCC_WARN_UNUSED_FUNCTION = YES; 636 | GCC_WARN_UNUSED_VARIABLE = YES; 637 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 638 | MACOSX_DEPLOYMENT_TARGET = 14.5; 639 | MTL_ENABLE_DEBUG_INFO = NO; 640 | MTL_FAST_MATH = YES; 641 | SDKROOT = macosx; 642 | SWIFT_COMPILATION_MODE = wholemodule; 643 | }; 644 | name = Release; 645 | }; 646 | 9D8C8DDA2C01ACF1003D8A43 /* Debug */ = { 647 | isa = XCBuildConfiguration; 648 | buildSettings = { 649 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 650 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 651 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 652 | CODE_SIGN_ENTITLEMENTS = MacTcode/MacTcode.entitlements; 653 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 654 | CODE_SIGN_STYLE = Automatic; 655 | COMBINE_HIDPI_IMAGES = YES; 656 | CURRENT_PROJECT_VERSION = 1; 657 | DEAD_CODE_STRIPPING = YES; 658 | DEVELOPMENT_ASSET_PATHS = "\"MacTcode/Preview Content\""; 659 | DEVELOPMENT_TEAM = 8H7RHH924X; 660 | ENABLE_HARDENED_RUNTIME = YES; 661 | ENABLE_PREVIEWS = YES; 662 | GENERATE_INFOPLIST_FILE = YES; 663 | INFOPLIST_FILE = MacTcode/Info.plist; 664 | INFOPLIST_KEY_CFBundleDisplayName = MacTcode; 665 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 666 | INFOPLIST_KEY_LSBackgroundOnly = YES; 667 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2024 Kaoru Maeda"; 668 | INFOPLIST_KEY_NSPrincipalClass = MacTcode.NSManualApplication; 669 | LD_RUNPATH_SEARCH_PATHS = ( 670 | "$(inherited)", 671 | "@executable_path/../Frameworks", 672 | ); 673 | MACOSX_DEPLOYMENT_TARGET = 11.0; 674 | MARKETING_VERSION = 0.8.5; 675 | ONLY_ACTIVE_ARCH = NO; 676 | PRODUCT_BUNDLE_IDENTIFIER = "jp.mad-p.inputmethod.MacTcode"; 677 | PRODUCT_NAME = "$(TARGET_NAME)"; 678 | "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = ENABLE_NSLOG; 679 | SWIFT_EMIT_LOC_STRINGS = YES; 680 | SWIFT_VERSION = 5.0; 681 | }; 682 | name = Debug; 683 | }; 684 | 9D8C8DDB2C01ACF1003D8A43 /* Release */ = { 685 | isa = XCBuildConfiguration; 686 | buildSettings = { 687 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 688 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 689 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; 690 | CODE_SIGN_ENTITLEMENTS = MacTcode/MacTcode.entitlements; 691 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; 692 | CODE_SIGN_STYLE = Automatic; 693 | COMBINE_HIDPI_IMAGES = YES; 694 | CURRENT_PROJECT_VERSION = 1; 695 | DEAD_CODE_STRIPPING = YES; 696 | DEVELOPMENT_ASSET_PATHS = "\"MacTcode/Preview Content\""; 697 | DEVELOPMENT_TEAM = 8H7RHH924X; 698 | ENABLE_HARDENED_RUNTIME = YES; 699 | ENABLE_PREVIEWS = YES; 700 | GENERATE_INFOPLIST_FILE = YES; 701 | INFOPLIST_FILE = MacTcode/Info.plist; 702 | INFOPLIST_KEY_CFBundleDisplayName = MacTcode; 703 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 704 | INFOPLIST_KEY_LSBackgroundOnly = YES; 705 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright (c) 2024 Kaoru Maeda"; 706 | INFOPLIST_KEY_NSPrincipalClass = MacTcode.NSManualApplication; 707 | LD_RUNPATH_SEARCH_PATHS = ( 708 | "$(inherited)", 709 | "@executable_path/../Frameworks", 710 | ); 711 | MACOSX_DEPLOYMENT_TARGET = 11.0; 712 | MARKETING_VERSION = 0.8.5; 713 | PRODUCT_BUNDLE_IDENTIFIER = "jp.mad-p.inputmethod.MacTcode"; 714 | PRODUCT_NAME = "$(TARGET_NAME)"; 715 | SWIFT_EMIT_LOC_STRINGS = YES; 716 | SWIFT_VERSION = 5.0; 717 | }; 718 | name = Release; 719 | }; 720 | 9D8C8DDD2C01ACF1003D8A43 /* Debug */ = { 721 | isa = XCBuildConfiguration; 722 | buildSettings = { 723 | BUNDLE_LOADER = "$(TEST_HOST)"; 724 | CODE_SIGN_STYLE = Automatic; 725 | CURRENT_PROJECT_VERSION = 1; 726 | DEAD_CODE_STRIPPING = YES; 727 | DEVELOPMENT_TEAM = 8H7RHH924X; 728 | GENERATE_INFOPLIST_FILE = YES; 729 | MACOSX_DEPLOYMENT_TARGET = 14.5; 730 | MARKETING_VERSION = 1.0; 731 | PRODUCT_BUNDLE_IDENTIFIER = "io.github.mad-p.MacTcodeTests"; 732 | PRODUCT_NAME = "$(TARGET_NAME)"; 733 | SWIFT_EMIT_LOC_STRINGS = NO; 734 | SWIFT_VERSION = 5.0; 735 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MacTcode.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MacTcode"; 736 | }; 737 | name = Debug; 738 | }; 739 | 9D8C8DDE2C01ACF1003D8A43 /* Release */ = { 740 | isa = XCBuildConfiguration; 741 | buildSettings = { 742 | BUNDLE_LOADER = "$(TEST_HOST)"; 743 | CODE_SIGN_STYLE = Automatic; 744 | CURRENT_PROJECT_VERSION = 1; 745 | DEAD_CODE_STRIPPING = YES; 746 | DEVELOPMENT_TEAM = 8H7RHH924X; 747 | GENERATE_INFOPLIST_FILE = YES; 748 | MACOSX_DEPLOYMENT_TARGET = 14.5; 749 | MARKETING_VERSION = 1.0; 750 | PRODUCT_BUNDLE_IDENTIFIER = "io.github.mad-p.MacTcodeTests"; 751 | PRODUCT_NAME = "$(TARGET_NAME)"; 752 | SWIFT_EMIT_LOC_STRINGS = NO; 753 | SWIFT_VERSION = 5.0; 754 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MacTcode.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MacTcode"; 755 | }; 756 | name = Release; 757 | }; 758 | 9D8C8DE02C01ACF1003D8A43 /* Debug */ = { 759 | isa = XCBuildConfiguration; 760 | buildSettings = { 761 | CODE_SIGN_STYLE = Automatic; 762 | CURRENT_PROJECT_VERSION = 1; 763 | DEAD_CODE_STRIPPING = YES; 764 | DEVELOPMENT_TEAM = 8H7RHH924X; 765 | GENERATE_INFOPLIST_FILE = YES; 766 | MARKETING_VERSION = 1.0; 767 | PRODUCT_BUNDLE_IDENTIFIER = "io.github.mad-p.MacTcodeUITests"; 768 | PRODUCT_NAME = "$(TARGET_NAME)"; 769 | SWIFT_EMIT_LOC_STRINGS = NO; 770 | SWIFT_VERSION = 5.0; 771 | TEST_TARGET_NAME = MacTcode; 772 | }; 773 | name = Debug; 774 | }; 775 | 9D8C8DE12C01ACF1003D8A43 /* Release */ = { 776 | isa = XCBuildConfiguration; 777 | buildSettings = { 778 | CODE_SIGN_STYLE = Automatic; 779 | CURRENT_PROJECT_VERSION = 1; 780 | DEAD_CODE_STRIPPING = YES; 781 | DEVELOPMENT_TEAM = 8H7RHH924X; 782 | GENERATE_INFOPLIST_FILE = YES; 783 | MARKETING_VERSION = 1.0; 784 | PRODUCT_BUNDLE_IDENTIFIER = "io.github.mad-p.MacTcodeUITests"; 785 | PRODUCT_NAME = "$(TARGET_NAME)"; 786 | SWIFT_EMIT_LOC_STRINGS = NO; 787 | SWIFT_VERSION = 5.0; 788 | TEST_TARGET_NAME = MacTcode; 789 | }; 790 | name = Release; 791 | }; 792 | /* End XCBuildConfiguration section */ 793 | 794 | /* Begin XCConfigurationList section */ 795 | 9D8C8DAF2C01ACEE003D8A43 /* Build configuration list for PBXProject "MacTcode" */ = { 796 | isa = XCConfigurationList; 797 | buildConfigurations = ( 798 | 9D8C8DD72C01ACF1003D8A43 /* Debug */, 799 | 9D8C8DD82C01ACF1003D8A43 /* Release */, 800 | ); 801 | defaultConfigurationIsVisible = 0; 802 | defaultConfigurationName = Release; 803 | }; 804 | 9D8C8DD92C01ACF1003D8A43 /* Build configuration list for PBXNativeTarget "MacTcode" */ = { 805 | isa = XCConfigurationList; 806 | buildConfigurations = ( 807 | 9D8C8DDA2C01ACF1003D8A43 /* Debug */, 808 | 9D8C8DDB2C01ACF1003D8A43 /* Release */, 809 | ); 810 | defaultConfigurationIsVisible = 0; 811 | defaultConfigurationName = Release; 812 | }; 813 | 9D8C8DDC2C01ACF1003D8A43 /* Build configuration list for PBXNativeTarget "MacTcodeTests" */ = { 814 | isa = XCConfigurationList; 815 | buildConfigurations = ( 816 | 9D8C8DDD2C01ACF1003D8A43 /* Debug */, 817 | 9D8C8DDE2C01ACF1003D8A43 /* Release */, 818 | ); 819 | defaultConfigurationIsVisible = 0; 820 | defaultConfigurationName = Release; 821 | }; 822 | 9D8C8DDF2C01ACF1003D8A43 /* Build configuration list for PBXNativeTarget "MacTcodeUITests" */ = { 823 | isa = XCConfigurationList; 824 | buildConfigurations = ( 825 | 9D8C8DE02C01ACF1003D8A43 /* Debug */, 826 | 9D8C8DE12C01ACF1003D8A43 /* Release */, 827 | ); 828 | defaultConfigurationIsVisible = 0; 829 | defaultConfigurationName = Release; 830 | }; 831 | /* End XCConfigurationList section */ 832 | }; 833 | rootObject = 9D8C8DAC2C01ACEE003D8A43 /* Project object */; 834 | } 835 | -------------------------------------------------------------------------------- /MacTcode.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MacTcode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MacTcode.xcodeproj/xcshareddata/xcschemes/MacTcode.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 36 | 42 | 43 | 44 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 69 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /MacTcode.xcodeproj/xcuserdata/maeda.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | MacTcode.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 9D8C8DB32C01ACEE003D8A43 16 | 17 | primary 18 | 19 | 20 | 9D8C8DC42C01ACF1003D8A43 21 | 22 | primary 23 | 24 | 25 | 9D8C8DCE2C01ACF1003D8A43 26 | 27 | primary 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /MacTcode/Action.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Action.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/06/01. 6 | // 7 | 8 | import Cocoa 9 | 10 | /// キーマップに登録するアクションの抽象クラス 11 | protocol Action { 12 | func execute(client: Client, mode: Mode, controller: Controller) -> Command 13 | } 14 | -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "black16.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "white16.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "black32.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "white32.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "idiom" : "universal", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "appearances" : [ 41 | { 42 | "appearance" : "luminosity", 43 | "value" : "dark" 44 | } 45 | ], 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIcon.imageset/black16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/MenuIcon.imageset/black16.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIcon.imageset/black32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/MenuIcon.imageset/black32.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIcon.imageset/white16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/MenuIcon.imageset/white16.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIcon.imageset/white32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/MenuIcon.imageset/white32.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIconDark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "white16.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "white32.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIconDark.imageset/white16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/MenuIconDark.imageset/white16.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIconDark.imageset/white32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/MenuIconDark.imageset/white32.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIconLight.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "black16.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "black32.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIconLight.imageset/black16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/MenuIconLight.imageset/black16.png -------------------------------------------------------------------------------- /MacTcode/Assets.xcassets/MenuIconLight.imageset/black32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/Assets.xcassets/MenuIconLight.imageset/black32.png -------------------------------------------------------------------------------- /MacTcode/Bushu/Bushu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TcodeBushu.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/05/27. 6 | // 7 | 8 | // 部首変換アルゴリズム 9 | // tc-bushu.el の初期バージョンのアルゴリズムを再現。 10 | // 元コードはGPLだが、コードコピーはしていないので、MITライセンスで配布できるはず。 11 | 12 | import Cocoa 13 | import InputMethodKit 14 | 15 | final class Bushu { 16 | static let i = Bushu() 17 | 18 | private var composeTable: [[String]: String] = [:] 19 | private var decomposeTable: [String: [String]] = [:] 20 | private var equivTable: [String: String] = [:] 21 | 22 | func readDictionary() { 23 | Log.i("Read bushu dictionary...") 24 | composeTable = [:] 25 | decomposeTable = [:] 26 | equivTable = [:] 27 | if let bushuDic = Config.loadConfig(file: "bushu.dic") { 28 | for line in bushuDic.components(separatedBy: .newlines) { 29 | let chars = line.map {String($0)} 30 | if chars.count == 3 { 31 | if chars[0] == "N" { 32 | equivTable[chars[2]] = chars[1] 33 | } else { 34 | let pair = [chars[0], chars[1]] 35 | decomposeTable[chars[2]] = pair 36 | composeTable[pair] = chars[2] 37 | } 38 | } else { 39 | if line.count > 0 { 40 | Log.i("Invalid bushu.dic entry: \(line)") 41 | } 42 | } 43 | } 44 | } 45 | Log.i("\(composeTable.count) bushu entries read") 46 | } 47 | 48 | private init() { 49 | readDictionary() 50 | } 51 | 52 | func basicCompose(char1: String, char2: String) -> String? { 53 | return (composeTable[[char1, char2]] ?? 54 | composeTable[[char2, char1]]) 55 | } 56 | 57 | func compose(char1: String, char2: String) -> String? { 58 | if let ch = basicCompose(char1: char1, char2: char2) { 59 | return ch 60 | } 61 | let ch1 = equivTable[char1] ?? char1 62 | let ch2 = equivTable[char2] ?? char2 63 | if ((ch1 != char1) || (ch2 != char2)) { 64 | if let ch = basicCompose(char1: ch1, char2: ch2) { 65 | return ch 66 | } 67 | } 68 | let tc1 = decomposeTable[ch1] 69 | let tc2 = decomposeTable[ch2] 70 | let tc11 = tc1?[0] 71 | let tc12 = tc1?[1] 72 | let tc21 = tc2?[0] 73 | let tc22 = tc2?[1] 74 | // subtraction 75 | if (tc11 == ch2) && (tc12 != ch1) && (tc12 != ch2) { 76 | return tc12 77 | } 78 | if (tc12 == ch2) && (tc11 != ch1) && (tc11 != ch2) { 79 | return tc11 80 | } 81 | if (tc21 == ch1) && (tc22 != ch1) && (tc22 != ch2) { 82 | return tc22 83 | } 84 | if (tc22 == ch1) && (tc21 != ch1) && (tc21 != ch2) { 85 | return tc21 86 | } 87 | // parts-wise composition 88 | for pair in [[ch1, tc22], [ch2, tc11], [ch1, tc21], [ch2, tc12], 89 | [tc12, tc22], [tc21, tc12], [tc11, tc22], [tc21, tc11]] { 90 | let p1 = pair[0] 91 | let p2 = pair[1] 92 | if p1 != nil && p2 != nil { 93 | if let ch = basicCompose(char1: p1!, char2: p2!) { 94 | if (ch != ch1) && (ch != ch2) { 95 | return ch 96 | } 97 | } 98 | } 99 | } 100 | // new subtraction 101 | if (tc11 != nil) && (tc11 == tc21) && (tc12 != ch1) && (tc12 != ch2) { 102 | return tc12 103 | } 104 | if (tc11 != nil) && (tc11 == tc22) && (tc12 != ch1) && (tc12 != ch2) { 105 | return tc12 106 | } 107 | if (tc12 != nil) && (tc12 == tc21) && (tc11 != ch1) && (tc11 != ch2) { 108 | return tc11 109 | } 110 | if (tc12 != nil) && (tc12 == tc22) && (tc11 != ch1) && (tc11 != ch2) { 111 | return tc11 112 | } 113 | // not found 114 | return nil 115 | } 116 | } 117 | 118 | class PostfixBushuAction: Action { 119 | func execute(client: Client, mode: Mode, controller: Controller) -> Command { 120 | // postfix bushu 121 | guard let client = client as? ContextClient else { 122 | Log.i("★★Can't happen: PostfixBushuAction.execute: client is not ContextClient") 123 | return .processed 124 | } 125 | let yomi = client.getYomi(2, 2) 126 | if yomi.string.count != 2 { 127 | Log.i("Bushu henkan: no input") 128 | return .processed 129 | } 130 | let chars = yomi.string.map { String($0) } 131 | let ch1 = chars[0] 132 | let ch2 = chars[1] 133 | Log.i("Bushu \(ch1)\(ch2)") 134 | 135 | if let ch = Bushu.i.compose(char1: ch1, char2: ch2) { 136 | Log.i("Bushu \(ch1)\(ch2) -> \(ch)") 137 | client.replaceYomi(ch, length: 2, from: yomi) 138 | } else { 139 | Log.i("Bushu henkan no candidates for \(ch1)\(ch2)") 140 | } 141 | 142 | return .processed 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /MacTcode/Client/Client.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/06/04. 6 | // 7 | 8 | import Cocoa 9 | 10 | /// IMKTextInputのラッパー 11 | protocol Client { 12 | func selectedRange() -> NSRange 13 | func string( 14 | from range: NSRange, 15 | actualRange: NSRangePointer 16 | ) -> String! 17 | func insertText( 18 | _ string: String, 19 | replacementRange: NSRange 20 | ) 21 | func sendBackspace() 22 | } 23 | -------------------------------------------------------------------------------- /MacTcode/Client/ClientWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientWrapper.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/15. 6 | // 7 | 8 | import Cocoa 9 | import InputMethodKit 10 | 11 | /// IMKTextInputをMyInputTextに見せかけるラッパー 12 | class ClientWrapper: Client { 13 | let client: IMKTextInput 14 | init(_ client: IMKTextInput!) { 15 | self.client = client 16 | } 17 | func selectedRange() -> NSRange { 18 | return client.selectedRange() 19 | } 20 | func string( 21 | from range: NSRange, 22 | actualRange: NSRangePointer 23 | ) -> String! { 24 | return client.string(from: range, actualRange: actualRange) 25 | } 26 | func insertText( 27 | _ string: String, 28 | replacementRange rr: NSRange 29 | ) { 30 | client.insertText(string, replacementRange: rr) 31 | } 32 | func sendBackspace() { 33 | let keyCode: CGKeyCode = 0x33 34 | let backspaceDown = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: true) 35 | let backspaceUp = CGEvent(keyboardEventSource: nil, virtualKey: keyCode, keyDown: false) 36 | 37 | backspaceDown?.post(tap: .cghidEventTap) 38 | backspaceUp?.post(tap: .cghidEventTap) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MacTcode/Client/ContextClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextClient.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/04. 6 | // 7 | 8 | import Cocoa 9 | 10 | /// 部首変換、交ぜ書き変換への入力を取得するためのClient 11 | /// クライアントのカーソル周辺の文字列、もし得られなければRecentTextClientから取るClient 12 | class ContextClient: Client { 13 | var client: Client 14 | let recent: RecentTextClient 15 | var lastCursor: NSRange = NSRange(location: NSNotFound, length: NSNotFound) 16 | init(client: Client, recent: RecentTextClient) { 17 | self.client = client 18 | self.recent = recent 19 | } 20 | func selectedRange() -> NSRange { 21 | Log.i("★★Can't happen. ContextClient.selectedRange()") 22 | return client.selectedRange() 23 | } 24 | func string( 25 | from range: NSRange, 26 | actualRange: NSRangePointer 27 | ) -> String! { 28 | Log.i("★★Can't happen. ContextClient.string(from:actualRange:)") 29 | return client.string(from: range, actualRange: actualRange) 30 | } 31 | func insertText( 32 | _ string: String, 33 | replacementRange rr: NSRange 34 | ) { 35 | client.insertText(string, replacementRange: rr) 36 | if rr.length == NSNotFound { 37 | recent.append(string) 38 | } else { 39 | Log.i("★★Can't happen. ContextClient.insertText with range?") 40 | recent.replaceLast(length: rr.length, with: string) 41 | } 42 | } 43 | func sendBackspace() { 44 | Log.i("★★Can't happen. ContextClient.sendBackspace()") 45 | client.sendBackspace() 46 | } 47 | // カーソル直前にある読みを取得する 48 | // クライアントが対応していなければrecentから取る 49 | func getYomi(_ minLength: Int, _ maxLength: Int) -> YomiContext { 50 | let cursor = client.selectedRange() 51 | lastCursor = cursor 52 | Log.i("getYomi: cursor=\(cursor) minLength=\(minLength) maxLength=\(maxLength)") 53 | var replaceRange = NSRange(location: NSNotFound, length: NSNotFound) 54 | var location: Int 55 | var getLength = minLength 56 | var fromSelection: Bool 57 | var fromMirror: Bool 58 | let emptyYomiContext = YomiContext(string: "", range: cursor, fromSelection: true, fromMirror: false) 59 | // 選択がある場合 → 選択全体 60 | // 選択がない場合 61 | // カーソルの前に文字がある場合 62 | // min, maxの指定を満たす場合 → 取れるだけ取る 63 | // 満たさない場合 → 64 | // カーソルの前に文字がない場合 → ミラーから 65 | if cursor.location == NSNotFound { 66 | // カーソルが取得できないクライアント 67 | location = 0 68 | getLength = 0 69 | fromMirror = true 70 | fromSelection = false 71 | Log.i("No cursor. Using mirror") 72 | // fall through to get from mirror 73 | } else if (cursor.length != 0 && cursor.length != NSNotFound) { // cursor.location != NSNotFound 74 | // 選択がある 75 | if minLength == maxLength { 76 | // min = maxの場合は選択の長さがぴったりの場合のみ返す 77 | if cursor.length == minLength { 78 | location = cursor.location 79 | getLength = cursor.length 80 | fromSelection = true 81 | fromMirror = false 82 | Log.i("Selection exact desired length: location=\(location) length=\(getLength)") 83 | // fall through to get from selection 84 | } else { 85 | Log.i("Selection length doesn't match for requirement: no result") 86 | return emptyYomiContext 87 | } 88 | } else { 89 | // min < maxの場合 90 | // 選択がmin以上max以下の場合のみ選択から返す。それ以外は取得しない(変換しない) 91 | if cursor.length < minLength { 92 | Log.i("Selection length (\(cursor.length)) < minLength (\(minLength)): no result") 93 | return emptyYomiContext 94 | } else if cursor.length > maxLength { 95 | Log.i("Selection length (\(cursor.length)) > maxLength (\(maxLength)): no result") 96 | return emptyYomiContext 97 | } else { 98 | location = cursor.location 99 | getLength = cursor.length 100 | fromSelection = true 101 | fromMirror = false 102 | Log.i("Selection length matches minLength..maxLength: get all of selection") 103 | // fall through to get from selection 104 | } 105 | } 106 | } else { // cursor.location != NSNotFound && (cursor.length == 0 || cursor.length == NSNotFound) 107 | // カーソルは取得できるが選択はない場合 108 | if cursor.location >= minLength { 109 | // 最大maxLengthまで取る 110 | if cursor.location >= maxLength { 111 | getLength = maxLength 112 | } else { 113 | getLength = cursor.location 114 | } 115 | location = cursor.location - getLength 116 | fromMirror = false 117 | fromSelection = false 118 | Log.i("No selection, enough yomi: location=\(location) length=\(getLength)") 119 | // fall through to get from selection 120 | } else { 121 | // バッファ先頭であり読みがない、または 122 | // 読みが取れないクライアント(Google Docsなど) 123 | location = 0 124 | getLength = 0 125 | fromMirror = true 126 | fromSelection = false 127 | Log.i("No selection, not enough yomi. Using mirror") 128 | // fall through to get from mirror 129 | } 130 | } 131 | 132 | if !fromMirror { 133 | // クライアントから取得。選択中の場合はrangeが選択範囲となっているはず 134 | let range = NSRange(location: location, length: getLength) 135 | Log.i("Trying to get yomi from client: range=\(range)") 136 | if let text = client.string(from: range, actualRange: &replaceRange) { 137 | if text.count > 0 { 138 | // Google DocsやSlidesはZero-width spaceまたはアンダースコアを1文字返すことがある。 139 | // Gemini CLIやClaude CodeはUIで表示した文字列から取ってきているようだ。 140 | // その場合はミラーから取る 141 | if text != "\u{200b}" && // old Google Docs 142 | text != "_" && // Google Docs 143 | text != "\n\n" && // Gemini CLI or Claude Code for 部首 144 | text != "xt left)\n\n" && // Gemini CLI for 交ぜ書き 145 | text != "──────╯\n\n\n" // Claude Code for 交ぜ書き 146 | { 147 | Log.i("Yomi taken from client: text=\(text) at actualRange=\(replaceRange)") 148 | return YomiContext(string: text, range: replaceRange, fromSelection: fromSelection, fromMirror: fromMirror) 149 | } 150 | } 151 | } 152 | // else fall through 153 | } 154 | // ミラーから取得 155 | fromMirror = true 156 | if recent.text.count < minLength { 157 | Log.i("No yomi found from mirror: recent.text.count < minLength") 158 | return emptyYomiContext 159 | } 160 | getLength = if recent.text.count < maxLength { recent.text.count } else { maxLength } 161 | location = recent.text.count - getLength 162 | if let text = recent.string(from: NSRange(location: location, length: getLength), actualRange: &replaceRange) { 163 | if text.count > 0 { 164 | Log.i("Yomi taken from mirror: text=\(text) at \(replaceRange)") 165 | return YomiContext(string: text, range: replaceRange, fromSelection: false, fromMirror: fromMirror) 166 | } 167 | } 168 | Log.i("No yomi found from mirror") 169 | return emptyYomiContext 170 | } 171 | // Yomiの後ろ側からlength文字をstringで置きかえる 172 | func replaceYomi(_ string: String, length: Int, from yomiContext: YomiContext) { 173 | // yomiContext.range: 読みの位置 174 | var rr = yomiContext.range 175 | rr.location += rr.length - length 176 | rr.length = length 177 | if yomiContext.fromMirror { 178 | // Mirrorから読みを取った場合は、BackSpaceを送ってから文字列を送る 179 | if length < 10 { 180 | Log.i("Sending \(length) BackSpaces and then \(string)") 181 | let now = DispatchTime.now() 182 | for i in 0.. NSRange { 19 | return NSRange(location: text.count, length: 0) 20 | } 21 | func string( 22 | from range: NSRange, 23 | actualRange: NSRangePointer 24 | ) -> String! { 25 | var s = range.location 26 | if s < 0 { 27 | s = 0 28 | } 29 | var l = range.length 30 | if s + l > text.count { 31 | l = text.count - s 32 | } 33 | let from = text.index(text.startIndex, offsetBy: s) 34 | let to = text.index(from, offsetBy: l) 35 | actualRange.pointee.location = s 36 | actualRange.pointee.length = l 37 | return String(text[from.. m { 59 | let newStart = text.index(text.endIndex, offsetBy: -m) 60 | text.replaceSubrange(text.startIndex.. 0 { 65 | text.removeLast() 66 | } 67 | } 68 | func append(_ newString: String) { 69 | text.append(newString) 70 | trim() 71 | } 72 | func replaceLast(length: Int, with newString: String) { 73 | let start = text.index(text.endIndex, offsetBy: -length) 74 | text.replaceSubrange(start.. String? { 13 | if let configFilePath = Bundle.main.path(forResource: file, ofType: nil) { 14 | do { 15 | let configContent = try String(contentsOfFile: configFilePath, encoding: .utf8) 16 | return configContent 17 | } catch { 18 | Log.i("Failed to read file: \(error)") 19 | return nil 20 | } 21 | } else { 22 | Log.i("Config file \(file) not found in bundle") 23 | return nil 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MacTcode/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | InputMethodConnectionName 6 | $(PRODUCT_BUNDLE_IDENTIFIER)_Connection 7 | InputMethodServerControllerClass 8 | $(PRODUCT_MODULE_NAME).TcodeInputController 9 | NSExtension 10 | 11 | NSExtensionMainClass 12 | $(PRODUCT_MODULE_NAME).TcodeInputController 13 | NSExtensionPointIdentifier 14 | com.apple.input-method 15 | 16 | TISIconIsTemplate 17 | 18 | TISIntendedLanguage 19 | ja 20 | tsInputMethodAlternateIconFileKey 21 | mainw.tiff 22 | tsInputMethodCharacterRepertoireKey 23 | 24 | Latn 25 | 26 | tsInputMethodIconFileKey 27 | main.tiff 28 | 29 | 30 | -------------------------------------------------------------------------------- /MacTcode/Keymap/EmitPendingAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmitPendingAction.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/04. 6 | // 7 | 8 | import Foundation 9 | 10 | /// キーシーケンスのprefixとしてたまっているものを入力する 11 | class EmitPendingAction: Action { 12 | func execute(client: any Client, mode: Mode, controller: Controller) -> Command { 13 | if let pending = mode as? MultiStroke { 14 | let input = pending.pending 15 | if input.count > 0 { 16 | let str = input.map { $0.text ?? "" }.joined() 17 | return .text(str) 18 | } 19 | } 20 | return .text(" ") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MacTcode/Keymap/InputEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InputEvent.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/06/02. 6 | // 7 | 8 | import Cocoa 9 | 10 | /// 入力イベントタイプ 11 | enum InputEventType { 12 | /// プリンタブル文字 13 | case printable 14 | /// エンターキー 15 | case enter 16 | /// 矢印キー 17 | case left, right, up, down 18 | /// スペースバー 19 | case space 20 | /// deleteキーまたは ^H 21 | case delete 22 | /// escapeキーまたは^[ 23 | case escape 24 | /// control + ',./=-;` 25 | case control_punct 26 | /// tab 27 | case tab 28 | /// control_g 29 | case control_g 30 | /// それ以外 31 | case unknown 32 | } 33 | 34 | /// 入力イベント 35 | struct InputEvent: Hashable, CustomStringConvertible { 36 | /// タイプ 37 | var type: InputEventType 38 | /// キーに対応する文字がある場合、その文字 39 | var text: String? 40 | /// 元となったイベント 41 | var event: NSEvent? 42 | /// ログ用表現 43 | var description: String { 44 | let t = if text == nil { "" } else { ", \(text!)" } 45 | return "InputEvent(\(type)\(t))" 46 | } 47 | 48 | /// printable, control_punctのときのみtextを考慮する 49 | static func == (lhs: InputEvent, rhs: InputEvent) -> Bool { 50 | if lhs.type != rhs.type { 51 | return false 52 | } 53 | switch lhs.type { 54 | case .printable, .control_punct: 55 | return (lhs.text == rhs.text) 56 | default: 57 | return true 58 | } 59 | } 60 | 61 | /// printableのときのみtextを考慮する 62 | func hash(into hasher: inout Hasher) { 63 | hasher.combine(type) 64 | switch type { 65 | case .printable: 66 | hasher.combine(text) 67 | default: 68 | break 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /MacTcode/Keymap/Keymap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Keymap.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/06/01. 6 | // 7 | 8 | import Cocoa 9 | 10 | let nKeys = 40 11 | 12 | /// イベントからコマンドへの対応 13 | class Keymap { 14 | var name: String 15 | var map: [InputEvent: Command] = [:] 16 | init(_ name: String) { 17 | self.name = name 18 | } 19 | init(_ name: String, fromArray chars: [String]) { 20 | self.name = name 21 | precondition(chars.count == nKeys, "Keymap \(name) fromChars: must have \(nKeys) characters") 22 | for i in 0.. Command? { 57 | return map[input] 58 | } 59 | func add(_ key: InputEvent, _ entry: Command) { 60 | if map[key] != nil { 61 | Log.i("Keymap \(name) replace \(key) to new entry \(String(describing: entry))") 62 | } 63 | map[key] = entry 64 | } 65 | func replace(input: InputEvent, entry: Command?) { 66 | if let e = entry { 67 | add(input, e) 68 | } else { 69 | Log.i("Keymap \(name) undefine \(input)") 70 | map.removeValue(forKey: input) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /MacTcode/Keymap/KeymapResolver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeymapResolver.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/04. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 入力イベントの列からKeymapをたどる 11 | class KeymapResolver { 12 | /// keySequenceでキーマップを探索し、最初に未定義またはコマンドを見つけたエントリの場所 13 | /// @returns (i, key, map): 14 | /// - i: keySequence内のキーの位置 15 | /// - key: 見つけたエントリに対応するキー 16 | /// - map: mapをkeyで引くとエントリが得られる 17 | static func traverse(keySequence: [InputEvent], keymap: Keymap) -> (Int, InputEvent, Keymap) { 18 | var map = keymap 19 | var lastmap = keymap 20 | for i in 0.. depth \(i) last key \(keySequence[i]) in map \(map.name)") 32 | return (i, keySequence[i], map) 33 | } 34 | } else { 35 | // Log.i("traverse found first undefined: seq: \(keySequence) -> depth \(i) last key \(keySequence[i]) in map \(map.name)") 36 | // 途中で未定義キーに出会った 37 | return (i, keySequence[i], map) 38 | } 39 | } 40 | // 最後まで行ったがまだキーマップ 41 | let i = keySequence.count - 1 42 | // Log.i("traverse reached lastmap \(lastmap.name) i=\(i) event=\(keySequence[i])") 43 | return (i, keySequence[i], lastmap) 44 | } 45 | /// keySequenceでkeymapを探索し、最初に見つかったcommandを返す 46 | /// @returns: command 47 | /// - .pending: 定義済みシーケンスのprefix部分。次の入力がないと定まらない 48 | /// - .passthrough: このマップにそのシーケンスは定義されていない 49 | /// - .text: テキスト。 50 | /// - .action: アクション 51 | static func resolve(keySequence: [InputEvent], keymap: Keymap) -> Command { 52 | let (_, key, map) = traverse(keySequence: keySequence, keymap: keymap) 53 | if let entry = map.lookup(input: key) { 54 | switch entry { 55 | case .keymap(_): 56 | return .pending // 定義済みシーケンスのprefix部分 57 | default: 58 | return entry // 見つかった。シーケンスの途中でも見つかったらそれを返す 59 | } 60 | } else { 61 | return .passthrough // このキーマップにそのシーケンスはない 62 | } 63 | } 64 | static func replace(keySequence: [InputEvent], keymap: Keymap, entry newEntry: Command) { 65 | let (_, key, map) = traverse(keySequence: keySequence, keymap: keymap) 66 | if let entry = map.lookup(input: key) { 67 | switch entry { 68 | case .keymap(_): 69 | break // リプレースしようと思ったところにはすでにキーマップが入ってた 70 | default: 71 | map.replace(input: key, entry: newEntry) 72 | } 73 | } 74 | // 最後に探索した場所に追加する 75 | map.replace(input: key, entry: newEntry) 76 | } 77 | static func define(keys: [Int], keymap: Keymap, entry: Command) { 78 | let events = keys.map { InputEvent(type: .printable, text: Translator.keyToStr($0)) } 79 | replace(keySequence: events, keymap: keymap, entry: entry) 80 | } 81 | static func define(keys: [Int], keymap: Keymap, action: Action) { 82 | define(keys: keys, keymap: keymap, entry: Command.action(action)) 83 | } 84 | static func define(sequence: String, keymap: Keymap, entry: Command) { 85 | let events = sequence.map { InputEvent(type: .printable, text: String($0)) } 86 | replace(keySequence: events, keymap: keymap, entry: entry) 87 | } 88 | 89 | static func define(sequence: String, keymap: Keymap, action: Action) { 90 | define(sequence: sequence, keymap: keymap, entry: Command.action(action)) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /MacTcode/Keymap/MultiStroke.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultiStroke.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/04. 6 | // 7 | 8 | import Foundation 9 | 10 | /// マルチストロークのキーマップを持つモード 11 | protocol MultiStroke { 12 | /// 複数キーコマンドの入力途中のイベント 13 | var pending: [InputEvent] { get } 14 | /// pending中のイベントをクリアする 15 | func resetPending() 16 | func removeLastPending() 17 | } 18 | -------------------------------------------------------------------------------- /MacTcode/Keymap/RemoveLastPendingAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoveLastPendingAction.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/04. 6 | // 7 | 8 | import Foundation 9 | 10 | /// キーシーケンスの途中だったら最後のものを消す 11 | /// シーケンスでなければ、入力イベントをそのままクライアントに渡す 12 | class RemoveLastPendingAction: Action { 13 | func execute(client: any Client, mode: Mode, controller: Controller) -> Command { 14 | if let pending = mode as? MultiStroke { 15 | if pending.pending.count > 0 { 16 | // pendingキーがあればひとつずつ消す 17 | pending.removeLastPending() 18 | return .processed 19 | } 20 | } 21 | // なければそのままクライアントに送る 22 | return .passthrough 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MacTcode/Keymap/ResetAllStateAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResetAllStateAction.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/04. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 途中までのキーシーケンス、入力モードなどを全部キャンセルする 11 | class ResetAllStateAction: Action { 12 | func execute(client: any Client, mode: Mode, controller: Controller) -> Command { 13 | mode.reset() 14 | return .processed 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MacTcode/Keymap/Translator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Translator.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/04. 6 | // 7 | 8 | import Cocoa 9 | import InputMethodKit 10 | 11 | /// NSEventをInputEventに変換する 12 | class Translator { 13 | static var layout: [String] = [ 14 | "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", 15 | "'", ",", ".", "p", "y", "f", "g", "c", "r", "l", 16 | "a", "o", "e", "u", "i", "d", "h", "t", "n", "s", 17 | ";", "q", "j", "k", "x", "b", "m", "w", "v", "z", 18 | ] 19 | static func strToKey(_ string: String!) -> Int? { 20 | return layout.firstIndex(of: string) 21 | } 22 | static func keyToStr(_ key: Int) -> String? { 23 | if (0.. InputEvent { 30 | Log.i("event.keyCode = \(event.keyCode); event.characters = \(event.characters ?? "nil"); event.modifierFlags = \(event.modifierFlags)") 31 | 32 | let text = event.characters 33 | let printable = if text != nil { 34 | text!.allSatisfy({ $0.isLetter || $0.isNumber || $0.isPunctuation || $0.isMathSymbol }) 35 | } else { 36 | false 37 | } 38 | 39 | var flags = "" 40 | if event.modifierFlags.contains(.option) { 41 | flags.append(" options") 42 | } 43 | if event.modifierFlags.contains(.command) { 44 | flags.append(" command") 45 | } 46 | if event.modifierFlags.contains(.function) { 47 | flags.append(" function") 48 | } 49 | if event.modifierFlags.contains(.control) { 50 | flags.append(" control") 51 | } 52 | Log.i(" modifierFlags: \(flags)") 53 | 54 | var type: InputEventType = .unknown 55 | if event.modifierFlags.contains(.option) 56 | || event.modifierFlags.contains(.command) 57 | { 58 | type = .unknown 59 | } else if printable { 60 | if text != nil && " ',.-=/;".contains(text!) && event.modifierFlags.contains(.control) { 61 | type = .control_punct 62 | } else if text == "\u{07}" && event.modifierFlags.contains(.control) { 63 | type = .control_g 64 | } else if text == " " { 65 | type = .space 66 | } else { 67 | type = .printable 68 | } 69 | } else { 70 | switch(text) { 71 | case " ": type = .space 72 | case "\u{08}": type = .delete 73 | case "\n": type = .enter 74 | case "\u{1b}": type = .escape 75 | case "\u{07}": type = .control_g 76 | case "\u{09}": type = .tab 77 | default: 78 | Log.i("Translate by keycode") 79 | switch(Int(event.keyCode)) { 80 | case kVK_Return: type = .enter 81 | case kVK_Tab: type = .tab 82 | case kVK_LeftArrow: type = .left 83 | case kVK_RightArrow: type = .right 84 | case kVK_DownArrow: type = .down 85 | case kVK_UpArrow: type = .up 86 | case kVK_Delete: type = .delete 87 | default: 88 | Log.i(" unknown keycode") 89 | type = .unknown 90 | } 91 | } 92 | } 93 | let event = InputEvent(type: type, text: text, event: event) 94 | Log.i(" translated to \(event)") 95 | return event 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /MacTcode/Log.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Log.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/06/09. 6 | // 7 | 8 | import Cocoa 9 | 10 | /// ログ出力 11 | class Log { 12 | static func i(_ message: String) { 13 | #if ENABLE_NSLOG 14 | NSLog (message) 15 | #endif 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MacTcode/MacTcode.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.temporary-exception.mach-register.global-name 8 | $(PRODUCT_BUNDLE_IDENTIFIER)_Connection 9 | 10 | 11 | -------------------------------------------------------------------------------- /MacTcode/MacTcodeApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacTcodeApp.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/05/25. 6 | // 7 | 8 | import Cocoa 9 | import InputMethodKit 10 | 11 | // Copied from https://github.com/ensan-hcl/Typut (MIT License) 12 | 13 | 14 | /// アプリケーションのエントリポイント 15 | class NSManualApplication: NSApplication { 16 | private let appDelegate = AppDelegate() 17 | 18 | override init() { 19 | super.init() 20 | self.delegate = appDelegate 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | // No need for implementation 25 | fatalError("init(coder:) has not been implemented") 26 | } 27 | } 28 | 29 | /// main 30 | @main 31 | class AppDelegate: NSObject, NSApplicationDelegate { 32 | var server = IMKServer() 33 | var candidatesWindow = IMKCandidates() 34 | 35 | func applicationDidFinishLaunching(_ notification: Notification) { 36 | self.server = IMKServer(name: Bundle.main.infoDictionary?["InputMethodConnectionName"] as? String, bundleIdentifier: Bundle.main.bundleIdentifier) 37 | self.candidatesWindow = IMKCandidates(server: server, panelType: kIMKSingleRowSteppingCandidatePanel, styleType: kIMKMain) 38 | Log.i("★AppDelegate launched self=\(ObjectIdentifier(self))") 39 | 40 | // アクセシビリティ権限の確認 41 | let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] 42 | let trusted = AXIsProcessTrustedWithOptions(options) 43 | 44 | if trusted { 45 | Log.i("★アクセシビリティ権限が付与されている") 46 | } else { 47 | Log.i("★アクセシビリティ権限がない") 48 | } 49 | } 50 | 51 | func applicationWillTerminate(_ notification: Notification) { 52 | Log.i("★AppDelegate terminated self=\(ObjectIdentifier(self))") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MacTcode/Mazegaki/Mazegaki.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mazegaki.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/05/28. 6 | // 7 | 8 | // 交ぜ書き変換アルゴリズム 9 | // tc-mazegaki.el の割と新しいバージョンのアルゴリズムを再現。 10 | // 元コードはGPLだが、コードコピーはしていないので、MITライセンスで配布できるはず。 11 | 12 | import Cocoa 13 | import InputMethodKit 14 | 15 | final class MazegakiDict { 16 | static let i = MazegakiDict() 17 | 18 | var dict: [String: String] = [:] 19 | static let inflectionMark = "—" 20 | 21 | func readDictionary() { 22 | Log.i("Read mazegaki dictionary...") 23 | dict = [:] 24 | if let mazedic = Config.loadConfig(file: "mazegaki.dic") { 25 | for line in mazedic.components(separatedBy: .newlines) { 26 | let kv = line.components(separatedBy: " ") 27 | if kv.count == 2 { 28 | dict[kv[0]] = kv[1] 29 | } else { 30 | if line.count > 0 { 31 | Log.i("Invalid mazegaki.dic line: \(line)") 32 | } 33 | } 34 | } 35 | } 36 | Log.i("\(dict.count) mazegaki entries read") 37 | } 38 | 39 | private init() { 40 | readDictionary() 41 | } 42 | } 43 | 44 | /// 特定の読み、活用部分に対応する候補全体を表わす 45 | class MazegakiHit: Comparable { 46 | var yomi: [String] = [] 47 | var found: Bool = false // 見つかったかどうか 48 | var key: String = "" // dictを見るときのキー 49 | var length: Int = 0 // 読みの長さ 50 | var offset: Int = 0 // 活用部分の長さ 51 | var cache: [String]? = nil 52 | 53 | func candidates() -> [String] { 54 | if let ret = cache { 55 | return ret 56 | } 57 | if found { 58 | if let dictEntry = MazegakiDict.i.dict[key] { 59 | let inflection = yomi.suffix(offset).joined() 60 | var cand = dictEntry.components(separatedBy: "/") 61 | if !cand.isEmpty { 62 | cand = cand.filter({ $0 != ""}) 63 | cand = cand.map { $0 + inflection } 64 | cache = cand 65 | return cand 66 | } 67 | } 68 | } 69 | cache = [] 70 | return [] 71 | } 72 | func duplicate() -> MazegakiHit { 73 | let newHit = MazegakiHit() 74 | newHit.yomi = yomi 75 | newHit.found = found 76 | newHit.key = key 77 | newHit.length = length 78 | newHit.offset = offset 79 | return newHit 80 | } 81 | static func < (lhs: MazegakiHit, rhs: MazegakiHit) -> Bool { 82 | return lhs.offset < rhs.offset || 83 | lhs.length > rhs.length 84 | } 85 | 86 | static func == (lhs: MazegakiHit, rhs: MazegakiHit) -> Bool { 87 | return lhs.found == rhs.found && 88 | lhs.yomi == rhs.yomi && 89 | lhs.key == rhs.key && 90 | lhs.length == rhs.length && 91 | lhs.offset == rhs.offset 92 | } 93 | } 94 | 95 | class Mazegaki { 96 | static var maxInflection = 4 // 活用部分の最大長 97 | static var inflectionCharsMin = 0x3041 // 活用部分に許される文字コードポイントの下限 98 | static var inflectionCharsMax = 0x30fe // 活用部分に許される文字上限 99 | static var inflectionRange = inflectionCharsMin...inflectionCharsMax 100 | static var nonYomiCharacters = 101 | ["、", "。", ",", ".", "・", "「", "」", "(", ")"] // 読み部分に許されない文字 102 | 103 | let yomi: [String] // 読み部分の文字列、各要素は1文字 104 | let inflection: Bool // 活用語をさがすかどうか 105 | let fixed: Bool // 読み長さが固定かどうか 106 | let max: Int // 読みの最大長さ。fixedの場合はyomiの長さと同じ 107 | let context: YomiContext 108 | 109 | init(_ context: YomiContext, inflection: Bool) { 110 | self.context = context 111 | let text = context.string 112 | self.fixed = context.fromSelection 113 | yomi = text.map { String($0) } 114 | let l = yomi.count 115 | var m = l 116 | for i in 0.. String? { 128 | if i > yomi.count || i == 0 || offset >= i { 129 | return nil 130 | } 131 | var chars = yomi.suffix(i) 132 | if offset > 0 && chars.count > offset { 133 | let infChars = chars.suffix(offset) 134 | if !infChars.allSatisfy({ 135 | let charCode = $0.unicodeScalars.first!.value 136 | return Mazegaki.inflectionRange.contains(Int(charCode)) 137 | }) { 138 | return nil 139 | } 140 | chars = chars.dropLast(offset) 141 | chars.append(MazegakiDict.inflectionMark) 142 | } 143 | if chars.count > 0 { 144 | let res: String = chars.joined() 145 | // Log.i("Mazegaki.key: yomi=\(yomi.joined()) i=\(i) offset=\(offset) -> result=\(res)") 146 | return res 147 | } else { 148 | return nil 149 | } 150 | } 151 | 152 | /// 全候補の可能性をすべて数えあげる 153 | func find() -> [MazegakiHit] { 154 | // 活用しないとき 155 | // - 最大長さを見つける 156 | // 活用するとき 157 | // - 全体の長さが同じときに、活用部分の長さが短い順で全部見つける 158 | var result: [MazegakiHit] = [] 159 | let iRange = fixed ? [max] : (0.. Bool { 181 | if !hit.found || index >= hit.candidates().count { 182 | return false 183 | } 184 | return self.submit(hit: hit, string: hit.candidates()[index], client: client) 185 | } 186 | 187 | func submit(hit: MazegakiHit, string: String, client: Client) -> Bool { 188 | guard let client = client as? ContextClient else { 189 | Log.i("★★Can't happen: Mazegaki.submit: client is not ContextClient") 190 | return false 191 | } 192 | if !hit.found { 193 | return false 194 | } 195 | let length = hit.length 196 | Log.i("Kakutei \(string) client=\(type(of:client))") 197 | client.replaceYomi(string, length: length, from: context) 198 | return true 199 | } 200 | } 201 | 202 | class PostfixMazegakiAction: Action { 203 | let maxYomi = 10 204 | let inflection : Bool 205 | init(inflection: Bool) { 206 | self.inflection = inflection 207 | } 208 | func execute(client: Client, mode: Mode, controller: Controller) -> Command { 209 | // postfix bushu 210 | guard let client = client as? ContextClient else { 211 | Log.i("★★Can't happen: PostfixBushuAction: client is not ContextClient") 212 | return .processed 213 | } 214 | let context = client.getYomi(1, 10) 215 | if context.string.count < 1 { 216 | Log.i("Mazegaki henkan: no input") 217 | return .processed 218 | } 219 | let text = context.string 220 | let mazegaki = Mazegaki(context, inflection: inflection) 221 | if context.fromSelection { 222 | Log.i("Mazegaki: Offline from selection \(text)") 223 | } else { 224 | Log.i("Mazegaki: from \(text)") 225 | } 226 | 227 | let hits = mazegaki.find() 228 | if hits.isEmpty { 229 | return .processed 230 | } 231 | if !inflection && hits.count == 1 && hits[0].candidates().count == 1 { 232 | if mazegaki.submit(hit: hits[0], index: 0, client: client) { 233 | return .processed 234 | } 235 | } 236 | let newMode = MazegakiSelectionMode(controller: controller, mazegaki: mazegaki, hits: hits) 237 | controller.pushMode(newMode) 238 | newMode.showWindow() 239 | // Log.i("Mazegaki: more than one candidates") 240 | 241 | return .processed 242 | } 243 | } 244 | 245 | class MazegakiSelectionMode: Mode, ModeWithCandidates { 246 | let map = MazegakiSelectionMap.map 247 | let mazegaki: Mazegaki 248 | let hits: [MazegakiHit] 249 | let controller: Controller 250 | let candidateWindow: IMKCandidates 251 | var candidateString: String = "" 252 | var row: Int 253 | init(controller: Controller, mazegaki: Mazegaki!, hits: [MazegakiHit]) { 254 | self.controller = controller 255 | self.mazegaki = mazegaki 256 | self.candidateWindow = controller.candidateWindow 257 | self.hits = hits 258 | self.row = 0 259 | Log.i("MazegakiSelectionMode.init") 260 | } 261 | func showWindow() { 262 | candidateWindow.update() 263 | candidateWindow.show() 264 | } 265 | func handle(_ inputEvent: InputEvent, client: ContextClient!, controller: any Controller) -> Bool { 266 | // キーで選択して確定。右手ホームの4キーの後数字の1~0 267 | Log.i("MazegakiSelectionMode.handle: event=\(inputEvent) client=\(client!) controller=\(controller)") 268 | if let selectKeys = candidateWindow.selectionKeys() as? [Int] { 269 | Log.i(" selectKeys = \(selectKeys)") 270 | if let keyCode = inputEvent.event?.keyCode { 271 | Log.i(" keyCode = \(Int(keyCode))") 272 | if let index = selectKeys.firstIndex(of: Int(keyCode)) { 273 | Log.i(" index = \(index)") 274 | let candidates = hits[row].candidates() 275 | if index < candidates.count { 276 | if mazegaki.submit(hit: hits[row], index: index, client: client) { 277 | cancel() 278 | } 279 | } 280 | return true 281 | } 282 | } 283 | } 284 | if let command = map.lookup(input: inputEvent) { 285 | switch command { 286 | case .passthrough: 287 | break 288 | case .processed: 289 | break 290 | case .action(let action): 291 | Log.i("execute action \(action)") 292 | _ = action.execute(client: client, mode: self, controller: controller) 293 | break 294 | default: 295 | break 296 | } 297 | return true 298 | } 299 | switch inputEvent.type { 300 | case .printable, .enter, .left, .right, .up, .down, .space, .tab: 301 | if let event = inputEvent.event { 302 | Log.i("Forward to candidateWindow: \([event])") 303 | candidateWindow.interpretKeyEvents([event]) 304 | } 305 | return true 306 | case .delete, .escape, .control_g: 307 | cancel() 308 | return true 309 | case .control_punct, .unknown: 310 | return true 311 | } 312 | } 313 | func update() { 314 | candidateWindow.update() 315 | } 316 | func cancel() { 317 | Log.i("MazegakiSelectionMode.cancel") 318 | candidateWindow.hide() 319 | controller.popMode() 320 | } 321 | func reset() { 322 | } 323 | 324 | func candidates(_ sender: Any!) -> [Any]! { 325 | Log.i("MazegakiSelectionMode.candidates") 326 | return hits[row].candidates() 327 | } 328 | 329 | func candidateSelected(_ candidateString: NSAttributedString!, client: (any Client)!) { 330 | Log.i("candidateSelected \(candidateString.string)") 331 | _ = mazegaki.submit(hit: hits[row], string: candidateString.string, client: client) 332 | cancel() 333 | } 334 | 335 | func candidateSelectionChanged(_ candidateString: NSAttributedString!) { 336 | self.candidateString = candidateString.string 337 | Log.i("candidateSelectionChanged \(candidateString.string)") 338 | } 339 | } 340 | 341 | class MazegakiAction: Action { 342 | func action(client: any Client, mode: MazegakiSelectionMode, controller: any Controller) -> Command { 343 | return .passthrough 344 | } 345 | func execute(client: any Client, mode mode1: any Mode, controller: any Controller) -> Command { 346 | if let mode = mode1 as? MazegakiSelectionMode { 347 | return action(client: client, mode: mode, controller: controller) 348 | } 349 | return .passthrough 350 | } 351 | } 352 | 353 | class MazegakiSelectionCancelAction: MazegakiAction { 354 | override func action(client: any Client, mode: MazegakiSelectionMode, controller: any Controller) -> Command { 355 | Log.i("CancelAction") 356 | mode.cancel() 357 | return .processed 358 | } 359 | } 360 | 361 | class MazegakiSelectionKakuteiAction: MazegakiAction { 362 | override func action(client: any Client, mode: MazegakiSelectionMode, controller: any Controller) -> Command { 363 | Log.i("KakuteiAction") 364 | _ = mode.mazegaki.submit(hit: mode.hits[mode.row], string: mode.candidateString, client: client) 365 | mode.cancel() 366 | return .processed 367 | } 368 | } 369 | 370 | /// 次の候補セットに送る(いわゆる再変換) 371 | class MazegakiSelectionNextAction: MazegakiAction { 372 | override func action(client: any Client, mode: MazegakiSelectionMode, controller: any Controller) -> Command { 373 | if mode.row < mode.hits.count - 1 { 374 | mode.row += 1 375 | mode.update() 376 | } 377 | return .processed 378 | } 379 | } 380 | /// 直前の候補に戻る 381 | class MazegakiSelectionPreviousAction: MazegakiAction { 382 | override func action(client: any Client, mode: MazegakiSelectionMode, controller: any Controller) -> Command { 383 | if mode.row > 0 { 384 | mode.row -= 1 385 | mode.update() 386 | } 387 | return .processed 388 | } 389 | } 390 | /// 変換を最初からやり直す 391 | class MazegakiSelectionRestartAction: MazegakiAction { 392 | override func action(client: any Client, mode: MazegakiSelectionMode, controller: any Controller) -> Command { 393 | Log.i("RestartAction") 394 | mode.row = 0 395 | mode.update() 396 | return .processed 397 | } 398 | } 399 | /// 送りがな部分をのばす 400 | class MazegakiSelectionOkuriNobashiAction: MazegakiAction { 401 | override func action(client: any Client, mode: MazegakiSelectionMode, controller: any Controller) -> Command { 402 | if mode.mazegaki.inflection { 403 | let offset = mode.hits[mode.row].offset 404 | if offset < Mazegaki.maxInflection { 405 | if let newRow = ((mode.row+1).. Command { 419 | if mode.mazegaki.inflection { 420 | let offset = mode.hits[mode.row].offset 421 | if offset > 1 { 422 | if let index = (1...mode.row).first(where: { r in 423 | (0.."), entry: .action(MazegakiSelectionOkuriChijimeAction())) 454 | map.replace(input: InputEvent(type: .printable, text: "/"), entry: .action(MazegakiSelectionRestartAction())) 455 | return map 456 | }() 457 | } 458 | -------------------------------------------------------------------------------- /MacTcode/Mode/Controller.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Controller.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/04. 6 | // 7 | 8 | import Foundation 9 | import InputMethodKit 10 | 11 | /// モードを持つIMKController 12 | protocol Controller { 13 | var mode: Mode { get } 14 | func pushMode(_ mode: Mode) 15 | func popMode() 16 | var candidateWindow: IMKCandidates { get } 17 | } 18 | -------------------------------------------------------------------------------- /MacTcode/Mode/Mode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mode.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/06/04. 6 | // 7 | 8 | import Cocoa 9 | import InputMethodKit 10 | 11 | /// 入力モード 12 | protocol Mode { 13 | /// 入力イベントを処理する 14 | func handle(_ inputEvent: InputEvent, client: ContextClient!, controller: Controller) -> Bool 15 | /// すべての状態を初期状態にする 16 | func reset() 17 | } 18 | -------------------------------------------------------------------------------- /MacTcode/Mode/ModeWithCandidates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModeWithCandidates.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/04. 6 | // 7 | 8 | import Foundation 9 | 10 | /// 変換候補を持つモード 11 | protocol ModeWithCandidates { 12 | /// 変換候補Windowを表示する 13 | func showWindow() 14 | /// 変換候補を返す 15 | func candidates(_ sender: Any!) -> [Any]! 16 | /// 候補選択された 17 | func candidateSelected(_ candidateString: NSAttributedString!, client: Client!) 18 | /// 別の候補が選択された 19 | func candidateSelectionChanged(_ candidateString: NSAttributedString!) 20 | } 21 | -------------------------------------------------------------------------------- /MacTcode/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MacTcode/Tcode/TcodeInputController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TcodeInputController.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/05/26. 6 | // 7 | 8 | import Cocoa 9 | import InputMethodKit 10 | 11 | @objc(TcodeInputController) 12 | class TcodeInputController: IMKInputController, Controller { 13 | var modeStack: [Mode] 14 | let candidateWindow: IMKCandidates 15 | let recentText = RecentTextClient("") 16 | var baseInputText: ContextClient? 17 | 18 | override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { 19 | modeStack = [TcodeMode()] 20 | candidateWindow = IMKCandidates(server: server, panelType: kIMKSingleRowSteppingCandidatePanel) 21 | super.init(server: server, delegate: delegate, client: inputClient) 22 | setupCandidateWindow() 23 | Log.i("★★TcodeInputController: init self=\(ObjectIdentifier(self))") 24 | } 25 | 26 | override func inputControllerWillClose() { 27 | Log.i("★★TcodeInputController: inputControllerWillClose self=\(ObjectIdentifier(self))") 28 | super.inputControllerWillClose() 29 | } 30 | 31 | func setupCandidateWindow() { 32 | let selectionKeys = [ 33 | kVK_ANSI_J, 34 | kVK_ANSI_K, 35 | kVK_ANSI_L, 36 | kVK_ANSI_Semicolon, 37 | kVK_ANSI_1, 38 | kVK_ANSI_2, 39 | kVK_ANSI_3, 40 | kVK_ANSI_4, 41 | kVK_ANSI_5, 42 | kVK_ANSI_6, 43 | kVK_ANSI_7, 44 | kVK_ANSI_8, 45 | kVK_ANSI_9, 46 | kVK_ANSI_0, 47 | ] 48 | candidateWindow.setSelectionKeys(selectionKeys) 49 | } 50 | 51 | override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { 52 | guard let client = sender as? IMKTextInput else { 53 | return false 54 | } 55 | // ログイン画面では変換しないでそのまま入力する 56 | Log.i("handle: client=\(type(of: client))") 57 | let bid = client.bundleIdentifier() 58 | Log.i(" client.bundleIdentifier=\(bid ?? "nil")") 59 | if bid == "com.apple.loginwindow" || bid == "com.apple.SecurityAgent" { 60 | return false 61 | } 62 | let inputEvent = Translator.translate(event: event) 63 | baseInputText = ContextClient(client: ClientWrapper(client), recent: recentText) 64 | return mode.handle(inputEvent, client: baseInputText, controller: self) 65 | } 66 | 67 | override func candidates(_ sender: Any!) -> [Any]! { 68 | if let modeWithCandidates = mode as? ModeWithCandidates { 69 | let ret = modeWithCandidates.candidates(sender) 70 | Log.i("TcodeInputController.candidates: returns \(ret!)") 71 | return ret 72 | } else { 73 | Log.i("*** TcodeInputController.candidates: called for non-ModeWithCandidates???") 74 | return [] 75 | } 76 | } 77 | 78 | override func candidateSelected(_ candidateString: NSAttributedString!) { 79 | Log.i("TcodeInputController.candidateSelected: \(candidateString.string)") 80 | if let modeWithCandidates = mode as? ModeWithCandidates { 81 | if let client = self.client() { 82 | if baseInputText != nil { 83 | baseInputText!.client = ClientWrapper(client) 84 | } else { 85 | baseInputText = ContextClient(client: ClientWrapper(client), recent: recentText) 86 | } 87 | modeWithCandidates.candidateSelected(candidateString, client: baseInputText) 88 | } else { 89 | Log.i("*** TcodeInputController.candidateSelected: client is not IMKTextInput???") 90 | } 91 | } else { 92 | Log.i("*** TcodeInputController.candidateSelected: called for non-ModeWithCandidates???") 93 | } 94 | } 95 | 96 | override func candidateSelectionChanged(_ candidateString: NSAttributedString!) { 97 | Log.i("TcodeInputController.candidateSelectionChanged: \(candidateString.string)") 98 | if let modeWithCandidates = mode as? ModeWithCandidates { 99 | modeWithCandidates.candidateSelectionChanged(candidateString) 100 | } else { 101 | Log.i("*** TcodeInputController.candidates: called for non-ModeWithCandidates???") 102 | } 103 | } 104 | 105 | var mode: Mode { 106 | get { 107 | modeStack.first! 108 | } 109 | } 110 | func pushMode(_ mode: Mode) { 111 | Log.i("TcodeInputController.pushMode: \(mode)") 112 | modeStack = [mode] + modeStack 113 | } 114 | func popMode() { 115 | if modeStack.count > 1 { 116 | modeStack.removeFirst() 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /MacTcode/Tcode/TcodeKeymap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TcodeTable.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/05/26. 6 | // 7 | 8 | import Cocoa 9 | 10 | class TcodeKeymap { 11 | static var map: Keymap = { 12 | let map = Keymap("TCode2D", from2d: """ 13 | ■■■■■■■■■■ヮヰヱヵヶ請境系探象ゎゐゑ■■盛革突温捕■■■■■依繊借須訳 14 | ■■■■■■■■■■丑臼宴縁曳尚賀岸責漁於汚乙穏■益援周域荒■■■■■織父枚乱香 15 | ■■■■■■■…■■鬼虚狭脅驚舎喜幹丘糖奇既菊却享康徒景処ぜ■■■■■譲ヘ模降走 16 | ■■■■■■■←■■孤誇黄后耕布苦圧恵固巧克懇困昏邦舞雑漢緊■■■■■激干彦均又 17 | ■■■■■■■■■■奉某貌卜■姿絶密秘押■■■■■衆節杉肉除■■■■■測血散笑弁 18 | ■■■■■■■■■■湖礼著移郷■■■■■償欧努底亜■■■■■禁硝樹句礎■■■■■ 19 | ■■■■■■■■■■端飾郵塩群■星析遷宣紅傷豪維脱鼠曹奏尊■絹被源願臨■■■■■ 20 | ■■→■■■■■<■刷寿順危砂庶粧丈称蒸舗充喫腕暴林薫■貢■批慶渉竜併■署↑■■ 21 | ■■■■■■■>■■震扱片札乞■乃如尼帳輪倒操柄魚■籍簿■■就駐揮丹鮮■■■■■ 22 | ■■■■■■■■■■弘痛票訴遺欄龍略慮累則存倍牛釈■■■■■綱潟創背皮■■■■■ 23 | ヲ哀暇啓把酸昼炭稲湯果告策首農歩回務島開報紙館夜位給員ど代レ欠夏彼妻善相家的対歴 24 | ゥ逢牙掲伐貿捜異隣旧概買詳由死キせ区百木音王放々応分よル千ア財針裏居差付プばュ作 25 | ヴ宛壊携避攻焼闘奈夕武残両在!や出タ手保案曲情引職7か(トれ従骨厚顔量内工八テ見 26 | ヂ囲較劇卑盤帯易速拡風階能論増コ山者発立横興刺側覚きっ日国二適類御宇推九名川機チ 27 | ヅ庵寒賢藩汽換延雪互細古利ペゃナ金マ和女崎白ぐ官球上く8え年母奥因酒伸サ建パ第入 28 | 簡徴触宗植■索射濁慢害賃整軽評佐法数郎談服声任検豊美題井洋実爆仲茶率比昔短岩巨敗 29 | 承章候途複■冊需詑迷撃折追隊角接備最急験変審改昇芸宿制集安画陽構旅施曜遠ォ将ぞ塚 30 | 快否歯筆里■皿輯蓄戻浴秀糸春幸記朝知ワ送限研労統役セ運ツ特谷ァ導認健尾序振練念働 31 | 包納頼逃寝■賛瞬貯羊積程断低減モ資士費ィ逆企精ざ印神び打勤ャ殺負何履般耳授版効視 32 | 唱暮憲勉罪■■盾虫■故鉱提児敷無石屋解募令違装然確優公品語演券悪秋非便示即難普辺 33 | ぱ慰我兼菱桜瀬鳥催障収際太園船中スもお定種岡結進真3と★てるヒ江別考権ッ人三京ち 34 | ぴ為掛嫌紐典博筋忠乳若雄査ふ賞わラ東生ろ宅熟待取科ーした一が及久蔵早造ロク万方フ 35 | ぷ陰敢顕描採謡希仏察指氏丸続ェう4)十リ料土活ね参い、の51投義算半県んまンつ四 36 | ぺ隠甘牽憤君純副盟標ぎ格次習火あこ6学月受予切育池。◆0・2込沢軍青清けイす電地 37 | ぽ胃患厳弊犯余堀肩療思術広門聞本さら高シ英ボ加室少ではになを転空性使級業時「長み 38 | 朱遅甲致汎■衰滋沈己病終起路越む南原駅物勢必講愛管要設水藤有素兵専親寮ホ共ブ平楽 39 | 陣鶴鹿貨絡■趨湿添已常張薬防得ケ式戦関男輸形助◇流連鉄教力ベ毛永申袋良私ゴ来信午 40 | 眼繁誌招季■垂甚徹巳寺質づ港条話座線ダ橋基好味宝争デ現エ他度等浅頃落命村ガ製校ご 41 | 執紹夢卸阿■粋■爪巴停領容玉右べ民ソ点遇足草築観言車成天世文板客師税飛ノ完重約各 42 | 岳刑弱雲窓■寸瞳陶■河置供試席期ゾ歳強係婦段衛額渋主映書可へ伝庭課着坂近外米ョ光 43 | ぁ■瓦■■呼幅歓功盗徳渡守登退店持町所ほ件友卒初慣行ド円小ジヨ誤証含%海道ず西げ 44 | ぃ■■■■紀破郡抗幡械刊訪融雨全じ自議明宮伊求技写通カ社野同判規感値ギ当理メウグ 45 | ぅ■■■■房績識属衣帝始了極熱バ部六経動局頭配黒院だり_め大済吉ゆ器照不合面政オ 46 | ぇ■■■■去疑ぢ綿離読鈴恐督況後間場ニ産向府富直倉新」9子五説週号葉派委化ビ目市 47 | ぉ○×☆□秒範核影麻族丁未才返問ム七住北割ぶ番望元事田会前そ休省央福毎気売下都株 48 | 欲巣茂述朗■■■■■帰庁昨跡ゲ洗羽個医静億録赤想消支協用表正図挙険ゼ波ヤ心界意今 49 | 迫災恋脳老■■■■■監寄裁達芝響忘討史環色貸販編仕先多商ハ交之末ぼ街免再ネ~口台 50 | 留列刻豆看■■↓■■竹注介具失司迎華許補左態花栄ザ調混ポ決ミ州払乗庫状団計夫食総 51 | 替沼?辞献■■■■■ゅ修究答養復並浦ユ冷ぬ展警型誰組選党択体例満津準遊戸ひょ価与 52 | 還更占箱矢■■■■■志抜航層深担陸巻競護根様独止堂銀以ヌ営治字材過諸単身ピ勝反ズ 53 | """) 54 | KeymapResolver.define(sequence: "hu", keymap: map, action: PostfixBushuAction()) 55 | KeymapResolver.define(sequence: "uh", keymap: map, action: PostfixMazegakiAction(inflection: false)) 56 | KeymapResolver.define(sequence: "58", keymap: map, action: PostfixMazegakiAction(inflection: true)) 57 | // ignore Ctrl-' 58 | map.replace(input: InputEvent(type: .control_punct, text: ","), entry: .processed) 59 | // passthrough Ctrl-SPC (set-mark) 60 | map.replace(input: InputEvent(type: .control_punct, text: " "), entry: .passthrough) 61 | KeymapResolver.define(sequence: "90", keymap: map, action: ZenkakuModeAction()) 62 | KeymapResolver.define(sequence: "\\", keymap: map, entry: Command.keymap( 63 | Keymap("outset1", fromChars: "√∂『』 “《》【】┏┳┓┃◎◆■●▲▼┣╋┫━ ◇□○△▽┗┻┛/\※§¶†‡"))) 64 | KeymapResolver.define(sequence: "\\\\", keymap: map, entry: Command.keymap( 65 | Keymap("outset2", fromChars: "♠♡♢♣㌧㊤㊥㊦㊧㊨㉖㉗㉘㉙㉚⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳①②③④⑤㉑㉒㉓㉔㉕⑥⑦⑧⑨⑩"))) 66 | 67 | return map 68 | }() 69 | } 70 | 71 | class TopLevelMap { 72 | static var map = { 73 | let map = Keymap("TopLevelMap") 74 | map.replace(input: InputEvent(type: .space, text: " "), entry: .action(EmitPendingAction())) 75 | map.replace(input: InputEvent(type: .escape, text: "\u{1b}"), entry: .action(ResetAllStateAction())) 76 | map.replace(input: InputEvent(type: .delete, text: "\u{08}"), entry: .action(RemoveLastPendingAction())) 77 | return map 78 | }() 79 | } 80 | -------------------------------------------------------------------------------- /MacTcode/Tcode/TcodeMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TcodeMode.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/05. 6 | // 7 | 8 | import Cocoa 9 | import InputMethodKit 10 | 11 | class TcodeMode: Mode, MultiStroke { 12 | var pending: [InputEvent] = [] 13 | var map = TcodeKeymap.map 14 | var quickMap: Keymap = TopLevelMap.map 15 | func resetPending() { 16 | pending = [] 17 | } 18 | func reset() { 19 | resetPending() 20 | } 21 | func removeLastPending() { 22 | if pending.count > 0 { 23 | pending.removeLast() 24 | } 25 | } 26 | func handle(_ inputEvent: InputEvent, client: ContextClient!, controller: Controller) -> Bool { 27 | let seq = pending + [inputEvent] 28 | 29 | // 複数キーからなるキーシーケンスの途中でも処理するコマンドはquickMapに入れておく 30 | // - spaceでpendingを送る 31 | // - escapeで取り消す 32 | // - deleteで1文字消す 33 | var command: Command? = 34 | if let topLevelEntry = quickMap.lookup(input: inputEvent) { 35 | topLevelEntry 36 | } else { 37 | KeymapResolver.resolve(keySequence: seq, keymap: map) 38 | } 39 | while command != nil { 40 | Log.i("execute command \(command!)") 41 | 42 | switch command! { 43 | case .passthrough: 44 | resetPending() 45 | return false 46 | case .processed: 47 | resetPending() 48 | return true 49 | case .pending: 50 | pending = seq 51 | return true 52 | case .text(let string): 53 | resetPending() 54 | client.insertText(string, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)) 55 | return true 56 | case .action(let action): 57 | command = action.execute(client: client, mode: self, controller: controller) 58 | resetPending() 59 | case .keymap(_): 60 | preconditionFailure("Keymap resolved to .keymap??") 61 | } 62 | } 63 | return true // can't happen 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /MacTcode/Zenkaku/ZenkakuMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZenkakuMode.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/06/05. 6 | // 7 | 8 | import Cocoa 9 | 10 | /// 全角入力モード 11 | class ZenkakuMode: Mode { 12 | static let zenkaku = { 13 | return " !”#$%&’()*+,−./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[¥]^_‘abcdefghijklmnopqrstuvwxyz{|} ̄" 14 | .map{ String($0) } 15 | }() 16 | 17 | func han2zen(_ string: String) -> String { 18 | return string.map { ch in 19 | if let ascii = ch.asciiValue { 20 | if (0x20..<0x7f).contains(ascii) { 21 | ZenkakuMode.zenkaku[Int(ascii - 0x20)] 22 | } else { 23 | String(ch) 24 | } 25 | } else { 26 | String(ch) 27 | } 28 | }.joined() 29 | } 30 | 31 | func handle(_ inputEvent: InputEvent, client: ContextClient!, controller: Controller) -> Bool { 32 | switch inputEvent.type { 33 | case .printable: 34 | if let inputString = inputEvent.text { 35 | let string = han2zen(inputString) 36 | client.insertText(string, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)) 37 | return true 38 | } 39 | return false 40 | case .escape: 41 | controller.popMode() 42 | return true 43 | default: 44 | return false 45 | } 46 | } 47 | 48 | func reset() { 49 | 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /MacTcode/Zenkaku/ZenkakuModeAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZenkakuModeAction.swift 3 | // MacTcode 4 | // 5 | // Created by maeda on 2024/08/04. 6 | // 7 | 8 | import Foundation 9 | 10 | class ZenkakuModeAction: Action { 11 | func execute(client: Client, mode: Mode, controller: Controller) -> Command { 12 | controller.pushMode(ZenkakuMode()) 13 | return .processed 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MacTcode/main.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/main.tiff -------------------------------------------------------------------------------- /MacTcode/main@2x.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/main@2x.tiff -------------------------------------------------------------------------------- /MacTcode/mainw.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/mainw.tiff -------------------------------------------------------------------------------- /MacTcode/mainw@2x.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/MacTcode/mainw@2x.tiff -------------------------------------------------------------------------------- /MacTcodeTests/BushuTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BushuTests.swift 3 | // MacTcodeTests 4 | // 5 | // Created by maeda on 2024/05/28. 6 | // 7 | 8 | import XCTest 9 | @testable import MacTcode 10 | 11 | final class BushuTests: XCTestCase { 12 | func testReadTable() { 13 | XCTAssertNotNil(Bushu.i) 14 | } 15 | func testBasicCompose() { 16 | XCTAssertEqual("照", Bushu.i.basicCompose(char1: "召", char2: "点"), "召点") 17 | XCTAssertEqual("照", Bushu.i.basicCompose(char1: "点", char2: "召"), "点召") 18 | } 19 | func testSubtraction() { 20 | XCTAssertEqual("圭", Bushu.i.compose(char1: "街", char2: "行"), "街行") 21 | XCTAssertEqual("圭", Bushu.i.compose(char1: "行", char2: "街"), "行街") 22 | XCTAssertEqual("口", Bushu.i.compose(char1: "鳴", char2: "鳥"), "鳴鳥") 23 | XCTAssertEqual("口", Bushu.i.compose(char1: "鳥", char2: "鳴"), "鳥鳴") 24 | XCTAssertEqual("豆", Bushu.i.compose(char1: "頭", char2: "顔"), "頭顔") 25 | XCTAssertEqual("彦", Bushu.i.compose(char1: "顔", char2: "頭"), "顔頭") 26 | 27 | } 28 | func testPartsWise() { 29 | XCTAssertEqual("記", Bushu.i.compose(char1: "語", char2: "起"), "語起") 30 | XCTAssertEqual("記", Bushu.i.compose(char1: "起", char2: "語"), "起語") 31 | XCTAssertEqual("悟", Bushu.i.compose(char1: "語", char2: "性"), "語性") 32 | XCTAssertEqual("悟", Bushu.i.compose(char1: "性", char2: "語"), "性語") 33 | XCTAssertEqual("貼", Bushu.i.compose(char1: "店", char2: "買"), "店買") 34 | XCTAssertEqual("貼", Bushu.i.compose(char1: "買", char2: "店"), "買店") 35 | 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /MacTcodeTests/ContextClientTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContextClientTests.swift 3 | // MacTcodeTests 4 | // 5 | // Created by maeda on 2024/08/17. 6 | // 7 | 8 | import XCTest 9 | @testable import MacTcode 10 | 11 | final class ContextClientTests: XCTestCase { 12 | class ClientStub: Client { 13 | var backSpaceCount = 0 14 | var insertedString = "" 15 | var insertedRange = NSRange(location: 0, length: 0) 16 | var insertCalled = false 17 | var insertCallback: () -> Void = {} 18 | var selectedRangeValue = NSRange(location: 0, length: 0) 19 | var selectedRangeCalled = false 20 | var stringReturnValue = "" 21 | var stringReturnRange = NSRange(location: 0, length: 0) 22 | var stringCalledRange = NSRange(location: 0, length: 0) 23 | var stringCalled = false 24 | 25 | init(selectedRangeValue: NSRange, 26 | stringReturnValue: String = "", 27 | stringReturnRange: NSRange = NSRange(location: 0, length: 0), 28 | insertCallback: @escaping () -> Void = {} 29 | ) { 30 | self.selectedRangeValue = selectedRangeValue 31 | self.stringReturnValue = stringReturnValue 32 | self.stringReturnRange = stringReturnRange 33 | self.insertCallback = insertCallback 34 | } 35 | func selectedRange() -> NSRange { 36 | selectedRangeCalled = true 37 | return selectedRangeValue 38 | } 39 | func string(from range: NSRange, actualRange: NSRangePointer) -> String! { 40 | stringCalled = true 41 | stringCalledRange = range 42 | actualRange.pointee = stringReturnRange 43 | return stringReturnValue 44 | } 45 | func insertText(_ string: String, replacementRange rr: NSRange) { 46 | insertCalled = true 47 | insertedString = string 48 | insertedRange = rr 49 | insertCallback() 50 | } 51 | func sendBackspace() { 52 | backSpaceCount += 1 53 | } 54 | } 55 | func testYomiExactFromSelection() { 56 | let client = ClientStub(selectedRangeValue: NSRange(location: 3, length: 2), 57 | stringReturnValue: "日月", 58 | stringReturnRange: NSRange(location: 3, length: 2)) 59 | let recent = RecentTextClient("あいうえお火水") 60 | let context = ContextClient(client: client, recent: recent) 61 | let yomi = context.getYomi(2, 2) 62 | XCTAssertEqual("日月", yomi.string) 63 | XCTAssertEqual(3, yomi.range.location) 64 | XCTAssertEqual(2, yomi.range.length) 65 | XCTAssertTrue(yomi.fromSelection) 66 | XCTAssertFalse(yomi.fromMirror) 67 | XCTAssertEqual(NSRange(location: 3, length: 2), client.selectedRangeValue) 68 | } 69 | func testYomiExactFromSelectionLengthMismatch() { 70 | let client = ClientStub(selectedRangeValue: NSRange(location: 3, length: 1), 71 | stringReturnValue: "月", 72 | stringReturnRange: NSRange(location: 3, length: 1)) 73 | let recent = RecentTextClient("あいうえお火水") 74 | let context = ContextClient(client: client, recent: recent) 75 | let yomi = context.getYomi(2, 2) 76 | XCTAssertEqual("", yomi.string) 77 | } 78 | func testYomiFromSelectionTooShort() { 79 | let client = ClientStub(selectedRangeValue: NSRange(location: 3, length: 1)) 80 | let recent = RecentTextClient("あいうえお火水") 81 | let context = ContextClient(client: client, recent: recent) 82 | let yomi = context.getYomi(2, 5) 83 | XCTAssertEqual("", yomi.string) 84 | } 85 | func testYomiFromSelectionTooLong() { 86 | let client = ClientStub(selectedRangeValue: NSRange(location: 3, length: 8)) 87 | let recent = RecentTextClient("あいうえお火水") 88 | let context = ContextClient(client: client, recent: recent) 89 | let yomi = context.getYomi(2, 5) 90 | XCTAssertEqual("", yomi.string) 91 | } 92 | func testYomiFromSelectionGoodLength() { 93 | let client = ClientStub(selectedRangeValue: NSRange(location: 3, length: 3), 94 | stringReturnValue: "日月火", 95 | stringReturnRange: NSRange(location: 3, length: 3)) 96 | let recent = RecentTextClient("あいうえお火水") 97 | let context = ContextClient(client: client, recent: recent) 98 | let yomi = context.getYomi(2, 5) 99 | XCTAssertEqual("日月火", yomi.string) 100 | XCTAssertEqual(3, yomi.range.location) 101 | XCTAssertEqual(3, yomi.range.length) 102 | XCTAssertTrue(yomi.fromSelection) 103 | XCTAssertFalse(yomi.fromMirror) 104 | XCTAssertEqual(NSRange(location: 3, length: 3), client.selectedRangeValue) 105 | } 106 | func testYomiExactFromClient() { 107 | let client = ClientStub(selectedRangeValue: NSRange(location: 33, length: 0), 108 | stringReturnValue: "日月", 109 | stringReturnRange: NSRange(location: 31, length: 2)) 110 | let recent = RecentTextClient("あいうえお火水") 111 | let context = ContextClient(client: client, recent: recent) 112 | let yomi = context.getYomi(2, 2) 113 | XCTAssertEqual("日月", yomi.string) 114 | XCTAssertEqual(31, yomi.range.location) 115 | XCTAssertEqual(2, yomi.range.length) 116 | XCTAssertFalse(yomi.fromSelection) 117 | XCTAssertFalse(yomi.fromMirror) 118 | XCTAssertTrue(client.stringCalled) 119 | XCTAssertEqual(NSRange(location: 31, length: 2), client.stringCalledRange) 120 | } 121 | func testYomiFromClient() { 122 | let client = ClientStub(selectedRangeValue: NSRange(location: 3, length: 0), 123 | stringReturnValue: "日月火", 124 | stringReturnRange: NSRange(location: 0, length: 3)) 125 | let recent = RecentTextClient("あいうえお火水") 126 | let context = ContextClient(client: client, recent: recent) 127 | let yomi = context.getYomi(1, 5) 128 | XCTAssertEqual("日月火", yomi.string) 129 | XCTAssertEqual(0, yomi.range.location) 130 | XCTAssertEqual(3, yomi.range.length) 131 | XCTAssertFalse(yomi.fromSelection) 132 | XCTAssertFalse(yomi.fromMirror) 133 | XCTAssertTrue(client.stringCalled) 134 | XCTAssertEqual(NSRange(location: 0, length: 3), client.stringCalledRange) 135 | } 136 | func testYomiExactFromClientNotEnoughText() { 137 | let client = ClientStub(selectedRangeValue: NSRange(location: 1, length: 0), 138 | stringReturnValue: "月", 139 | stringReturnRange: NSRange(location: 3, length: 1)) 140 | let recent = RecentTextClient("水") 141 | let context = ContextClient(client: client, recent: recent) 142 | let yomi = context.getYomi(2, 2) 143 | XCTAssertEqual("", yomi.string) 144 | } 145 | func testYomiFromClientNotEnoughText() { 146 | let client = ClientStub(selectedRangeValue: NSRange(location: 0, length: 0), 147 | stringReturnValue: "月", 148 | stringReturnRange: NSRange(location: 3, length: 1)) 149 | let recent = RecentTextClient("") 150 | let context = ContextClient(client: client, recent: recent) 151 | let yomi = context.getYomi(1, 10) 152 | XCTAssertEqual("", yomi.string) 153 | } 154 | func testYomiFromMirrorWhenClientHasNoCursor() { 155 | let client = ClientStub(selectedRangeValue: NSRange(location: NSNotFound, length: NSNotFound)) 156 | let recent = RecentTextClient("あいうえお火水") 157 | let context = ContextClient(client: client, recent: recent) 158 | let yomi = context.getYomi(2, 2) 159 | XCTAssertEqual("火水", yomi.string) 160 | XCTAssertEqual(5, yomi.range.location) 161 | XCTAssertEqual(2, yomi.range.length) 162 | XCTAssertFalse(yomi.fromSelection) 163 | XCTAssertTrue(yomi.fromMirror) 164 | } 165 | func testYomiFromMirrorWhenClientTextTooShort() { 166 | let client = ClientStub(selectedRangeValue: NSRange(location: 1, length: 0)) 167 | let recent = RecentTextClient("あいうえお火水") 168 | let context = ContextClient(client: client, recent: recent) 169 | let yomi = context.getYomi(2, 2) 170 | XCTAssertEqual("火水", yomi.string) 171 | XCTAssertEqual(5, yomi.range.location) 172 | XCTAssertEqual(2, yomi.range.length) 173 | XCTAssertFalse(yomi.fromSelection) 174 | XCTAssertTrue(yomi.fromMirror) 175 | } 176 | func testYomiFromMirrorWhenClientReturnsAZeroWidthSpace() { 177 | let client = ClientStub(selectedRangeValue: NSRange(location: 1, length: 0), 178 | stringReturnValue: "\u{200b}", 179 | stringReturnRange: NSRange(location: 0, length: 1)) 180 | let recent = RecentTextClient("あいうえお火水") 181 | let context = ContextClient(client: client, recent: recent) 182 | let yomi = context.getYomi(1, 5) 183 | XCTAssertEqual("うえお火水", yomi.string) 184 | XCTAssertEqual(2, yomi.range.location) 185 | XCTAssertEqual(5, yomi.range.length) 186 | XCTAssertFalse(yomi.fromSelection) 187 | XCTAssertTrue(yomi.fromMirror) 188 | XCTAssertTrue(client.stringCalled) 189 | XCTAssertEqual(NSRange(location: 0, length: 1), client.stringCalledRange) 190 | } 191 | func testYomiFromMirrorWhenClientReturnsEmptyString() { 192 | let client = ClientStub(selectedRangeValue: NSRange(location: 3, length: 0), 193 | stringReturnValue: "") 194 | let recent = RecentTextClient("あいうえお火水") 195 | let context = ContextClient(client: client, recent: recent) 196 | let yomi = context.getYomi(1, 3) 197 | XCTAssertEqual("お火水", yomi.string) 198 | XCTAssertEqual(4, yomi.range.location) 199 | XCTAssertEqual(3, yomi.range.length) 200 | XCTAssertFalse(yomi.fromSelection) 201 | XCTAssertTrue(yomi.fromMirror) 202 | } 203 | func testYomiFromMirrorWhenClientReturnsUnderscore() { 204 | let client = ClientStub(selectedRangeValue: NSRange(location: 0, length: 1), 205 | stringReturnValue: "_") 206 | let recent = RecentTextClient("あいうえお火水") 207 | let context = ContextClient(client: client, recent: recent) 208 | let yomi = context.getYomi(1, 3) 209 | XCTAssertEqual("お火水", yomi.string) 210 | XCTAssertEqual(4, yomi.range.location) 211 | XCTAssertEqual(3, yomi.range.length) 212 | XCTAssertFalse(yomi.fromSelection) 213 | XCTAssertTrue(yomi.fromMirror) 214 | } 215 | func testReplaceYomiFromSelection() { 216 | let client = ClientStub(selectedRangeValue: NSRange(location: 3, length: 2)) 217 | let recent = RecentTextClient("あいうえお火水") 218 | let context = ContextClient(client: client, recent: recent) 219 | context.replaceYomi("木金", length: 2, from: YomiContext(string: "火水", range: NSRange(location: 3, length: 2), fromSelection: true, fromMirror: false)) 220 | XCTAssertTrue(client.insertCalled) 221 | XCTAssertEqual("木金", client.insertedString) 222 | XCTAssertEqual(NSRange(location: 3, length: 2), client.insertedRange) 223 | XCTAssertEqual("あいうえお木金", recent.text) 224 | } 225 | func testReplaceYomiFromClient() { 226 | let client = ClientStub(selectedRangeValue: NSRange(location: 3, length: 0), 227 | stringReturnValue: "火水", 228 | stringReturnRange: NSRange(location: 1, length: 2)) 229 | let recent = RecentTextClient("あいうえお火水") 230 | let context = ContextClient(client: client, recent: recent) 231 | context.replaceYomi("木金", length: 2, from: YomiContext(string: "火水", range: NSRange(location: 1, length: 2), fromSelection: false, fromMirror: false)) 232 | XCTAssertTrue(client.insertCalled) 233 | XCTAssertEqual("木金", client.insertedString) 234 | XCTAssertEqual(NSRange(location: 1, length: 2), client.insertedRange) 235 | XCTAssertEqual("あいうえお木金", recent.text) 236 | } 237 | func testReplaceYomiFromMirror() { 238 | let expectation = XCTestExpectation(description: "insert called") 239 | let client = ClientStub(selectedRangeValue: NSRange(location: NSNotFound, length: NSNotFound)) { 240 | expectation.fulfill() 241 | } 242 | let recent = RecentTextClient("あいうえお火水") 243 | let context = ContextClient(client: client, recent: recent) 244 | context.replaceYomi("木金", length: 3, from: YomiContext(string: "あいうえお火水", range: NSRange(location: 0, length: 7), fromSelection: false, fromMirror: true)) 245 | wait(for: [expectation], timeout: 2.0) 246 | XCTAssertEqual(3, client.backSpaceCount) 247 | XCTAssertTrue(client.insertCalled) 248 | XCTAssertEqual("木金", client.insertedString) 249 | XCTAssertEqual(NSRange(location: NSNotFound, length: NSNotFound), client.insertedRange) 250 | XCTAssertEqual("あいうえ木金", recent.text) 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /MacTcodeTests/KeymapTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacTcodeTests.swift 3 | // MacTcodeTests 4 | // 5 | // Created by maeda on 2024/05/25. 6 | // 7 | 8 | import XCTest 9 | @testable import MacTcode 10 | 11 | final class KeymapTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | 22 | func testLookup() { 23 | let e1 = TcodeKeymap.map.lookup(input: InputEvent(type: .printable, text: "t")) 24 | switch e1 { 25 | case .keymap(let m2): 26 | let c2 = m2.lookup(input: InputEvent(type: .printable, text: "e")) 27 | switch c2 { 28 | case .text(let string): 29 | XCTAssertEqual("の", string) 30 | default: 31 | XCTFail() 32 | } 33 | default: 34 | XCTFail() 35 | } 36 | } 37 | 38 | 39 | func testKeymap1() { 40 | guard let k = Keymap("testmap", fromChars: "√∂『』 《》【】“┏┳┓┃◎◆■●▲▼┣╋┫━ ◇□○△▽┗┻┛/\※§¶†‡") 41 | else { 42 | XCTFail() 43 | return 44 | } 45 | let c1 = k.lookup(input: InputEvent(type: .printable, text: "i")) 46 | switch c1 { 47 | case .text(let string): 48 | XCTAssertEqual(" ", string) 49 | default: 50 | XCTFail() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MacTcodeTests/MazegakiSelectionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MazegakiSelectionTests.swift 3 | // MacTcodeTests 4 | // 5 | // Created by maeda on 2024/06/08. 6 | // 7 | 8 | 9 | import XCTest 10 | import Cocoa 11 | import InputMethodKit 12 | @testable import MacTcode 13 | 14 | final class MazegakiSelectionTests: XCTestCase { 15 | var spy: RecentTextClient! 16 | var client: ContextClient! 17 | var controller: ControllerSpy! 18 | 19 | class CandidateWindowSpy: IMKCandidates { 20 | var events: [NSEvent] = [] 21 | var shown: Bool = false 22 | override func setSelectionKeys(_ keyCodes: [Any]!) { 23 | } 24 | override func interpretKeyEvents(_ eventArray: [NSEvent]) { 25 | events = events + eventArray 26 | } 27 | override func show() { 28 | shown = true 29 | } 30 | override func show(_ locationHint: IMKCandidatesLocationHint) { 31 | shown = true 32 | } 33 | override func hide() { 34 | shown = false 35 | } 36 | } 37 | 38 | class ControllerSpy: Controller { 39 | var client: ContextClient 40 | var modeStack: [Mode] 41 | var mode: Mode { get { modeStack.first! } } 42 | var candidateWindow: IMKCandidates { get { window } } 43 | var window: CandidateWindowSpy = CandidateWindowSpy() 44 | init(mode: Mode, client: ContextClient) { 45 | modeStack = [mode] 46 | self.client = client 47 | } 48 | func pushMode(_ mode: Mode) { 49 | modeStack = [mode] + modeStack 50 | } 51 | func popMode() { 52 | modeStack.removeFirst() 53 | } 54 | func handle(_ event: NSEvent!, client sender: Any!) -> Bool { 55 | if let client = sender as? ContextClient { 56 | self.client = client 57 | } else { 58 | XCTFail() 59 | } 60 | let inputEvent = Translator.translate(event: event) 61 | return mode.handle(inputEvent, client: client, controller: self) 62 | } 63 | 64 | func candidates(_ sender: Any!) -> [Any]! { 65 | if let modeWithCandidates = mode as? ModeWithCandidates { 66 | return modeWithCandidates.candidates(sender) 67 | } else { 68 | XCTFail() 69 | return [] 70 | } 71 | } 72 | 73 | func candidateSelected(_ candidateString: NSAttributedString!) { 74 | if let modeWithCandidates = mode as? ModeWithCandidates { 75 | modeWithCandidates.candidateSelected(candidateString, client: client) 76 | } else { 77 | XCTFail() 78 | } 79 | } 80 | 81 | } 82 | 83 | func stubCharEvent(_ char: String) -> InputEvent { 84 | return Translator.translate(event: NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, characters: char, charactersIgnoringModifiers: char, isARepeat: false, keyCode: 0)!) 85 | } 86 | func stubCodeEvent(_ code: Int, char: String = "a") -> InputEvent { 87 | return Translator.translate(event: NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, characters: char, charactersIgnoringModifiers: char, isARepeat: false, keyCode: UInt16(code))!) 88 | } 89 | func feed(_ sequence: String) { 90 | sequence.forEach { char in 91 | let event = stubCharEvent(String(char)) 92 | let ret = controller.mode.handle(event, client: client, controller: controller) 93 | XCTAssertTrue(ret) 94 | } 95 | } 96 | 97 | override func setUpWithError() throws { 98 | super.setUp() 99 | spy = RecentTextClient("") 100 | client = ContextClient(client: spy, recent: RecentTextClient("")) 101 | controller = ControllerSpy(mode: TcodeMode(), client: client) 102 | Log.i("setUp!") 103 | } 104 | 105 | override func tearDownWithError() throws { 106 | super.tearDown() 107 | } 108 | 109 | func testWindowCreation() { 110 | XCTAssertFalse(controller.window.shown) 111 | feed("fusxfez,uh") 112 | XCTAssertTrue(controller.window.shown) 113 | XCTAssertEqual("あそう作", spy.text) 114 | } 115 | 116 | func testCandidates() { 117 | XCTAssertFalse(controller.window.shown) 118 | feed("fusxfez,uh") 119 | XCTAssertEqual(["創作", "操作"], controller.candidates(controller) as? [String]) 120 | } 121 | func testForwarding() { 122 | XCTAssertFalse(controller.window.shown) 123 | feed("fusxfez,uh") 124 | // case 38: .enter 125 | // case 123: .left 126 | // case 124: .right 127 | // case 125: .down 128 | // case 126: .up 129 | // case 51: .delete 130 | let events: [NSEvent] = [ 131 | NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, characters: "a", charactersIgnoringModifiers: "a", isARepeat: false, keyCode: 124)!, 132 | NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, characters: "a", charactersIgnoringModifiers: "a", isARepeat: false, keyCode: 38)!, 133 | NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, characters: "h", charactersIgnoringModifiers: "h", isARepeat: false, keyCode: UInt16(kVK_ANSI_J))! 134 | ] 135 | events.forEach { event in 136 | XCTAssertTrue(controller.handle(event, client: client)) 137 | } 138 | XCTAssertEqual(events[0].keyCode, controller.window.events[0].keyCode) 139 | XCTAssertEqual(events[1].keyCode, controller.window.events[1].keyCode) 140 | XCTAssertEqual(events[2].keyCode, controller.window.events[2].keyCode) 141 | } 142 | func testKakutei() { 143 | XCTAssertFalse(controller.window.shown) 144 | feed("fusxfez,uh") 145 | let event = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, characters: "a", charactersIgnoringModifiers: "a", isARepeat: false, keyCode: 124)! 146 | _ = controller.handle(event, client: client) 147 | let selected: NSAttributedString = NSAttributedString(string: "操作") 148 | controller.candidateSelected(selected) 149 | XCTAssertFalse(controller.window.shown) 150 | XCTAssertEqual("あ操作", spy.text) 151 | XCTAssertEqual(1, controller.modeStack.count) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /MacTcodeTests/MazegakiTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MazegakiTests.swift 3 | // MacTcodeTests 4 | // 5 | // Created by maeda on 2024/05/29. 6 | // 7 | 8 | import XCTest 9 | @testable import MacTcode 10 | 11 | final class MazegakiTests: XCTestCase { 12 | func testReadDict() { 13 | XCTAssertNotNil(MazegakiDict.i) 14 | XCTAssertNotNil(MazegakiDict.i.dict.count > 0) 15 | } 16 | func testLookup() { 17 | XCTAssertNotNil(MazegakiDict.i.dict["一らん"]) 18 | XCTAssertNil(MazegakiDict.i.dict["ほげら"]) 19 | } 20 | func y(_ string: String, fixed: Bool = false) -> YomiContext { 21 | return YomiContext(string: string, range: NSRange(location: 0, length: string.count), fromSelection: fixed, fromMirror: false) 22 | } 23 | func testMazegakiState() { 24 | let m = Mazegaki(y("今日は地しん"), inflection: false) 25 | XCTAssertEqual(m.max, 6) 26 | XCTAssertEqual("地しん", m.key(3)) 27 | XCTAssertEqual("地—", m.key(3, offset: 2)) 28 | XCTAssertNil(m.key(8)) 29 | XCTAssertNil(m.key(4, offset: 4)) 30 | } 31 | func testMazegakiFind() { 32 | let m1 = Mazegaki(y("今日のはにわ"), inflection: false) 33 | let rs1 = m1.find() 34 | XCTAssertEqual(3, rs1.count) 35 | 36 | let r1 = rs1[0] 37 | XCTAssertEqual("はにわ", r1.key) 38 | XCTAssertTrue(r1.found) 39 | XCTAssertEqual(3, r1.length) 40 | 41 | let r2 = rs1[1] 42 | XCTAssertEqual("にわ", r2.key) 43 | XCTAssertTrue(r2.found) 44 | XCTAssertEqual(2, r2.length) 45 | 46 | let r3 = rs1[2] 47 | XCTAssertEqual("わ", r3.key) 48 | XCTAssertTrue(r3.found) 49 | XCTAssertEqual(1, r3.length) 50 | 51 | let m2 = Mazegaki(y("今日は地信ん"), inflection: false) 52 | let rs2 = m2.find() 53 | XCTAssertEqual(0, rs2.count) 54 | } 55 | func testMazegakiFindFixed() { 56 | let m1 = Mazegaki(y("はにわ", fixed: true), inflection: false) 57 | let rs1 = m1.find() 58 | XCTAssertEqual(1, rs1.count) 59 | let r1 = rs1[0] 60 | XCTAssertEqual("はにわ", r1.key) 61 | XCTAssertTrue(r1.found) 62 | XCTAssertEqual(3, r1.length) 63 | } 64 | func testMazegakiNonYomiChars() { 65 | let m1 = Mazegaki(y("なんだ、さくじょ"), inflection: false) 66 | XCTAssertEqual(4, m1.max) 67 | } 68 | func testMazegakiHitCands() { 69 | let m1 = Mazegaki(y("地しん", fixed: true), inflection: false) 70 | let rs1 = m1.find() 71 | XCTAssertEqual(1, rs1.count) 72 | let c1 = rs1[0].candidates() 73 | XCTAssertEqual(["地震"], c1) 74 | let m2 = Mazegaki(y("そう作", fixed: true), inflection: false) 75 | let rs2 = m2.find() 76 | XCTAssertEqual(1, rs2.count) 77 | let c2 = rs2[0].candidates() 78 | XCTAssertEqual(Set(["操作", "創作"]), Set(c2)) 79 | } 80 | func testMazegakiInflection() { 81 | let m = Mazegaki(y("うけたまわる"), inflection: true) 82 | let rs = m.find() 83 | let expectedResult = ["承る", "賜る", "回る", "割る"] 84 | let nonExpectedResult = ["賜", "玉"] 85 | let allCandidates = rs.flatMap{ $0.candidates() } 86 | expectedResult.forEach { str in 87 | XCTAssertTrue(allCandidates.contains(str), "\(str) should be in candidates") 88 | } 89 | nonExpectedResult.forEach { str in 90 | XCTAssertFalse(allCandidates.contains(str), "\(str) should not be in candidates") 91 | } 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /MacTcodeTests/RecentTextTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecentTextTests.swift 3 | // MacTcodeTests 4 | // 5 | // Created by maeda on 2024/06/01. 6 | // 7 | 8 | import XCTest 9 | @testable import MacTcode 10 | 11 | final class RecentTextTests: XCTestCase { 12 | func testTrim() { 13 | let r = RecentTextClient("今日は晴れxでしたね") 14 | XCTAssertEqual("今日は晴れxでしたね", r.text) 15 | r.append("明日は雨かもyしれないですね") 16 | XCTAssertEqual("れxでしたね明日は雨かもyしれないですね", r.text) 17 | } 18 | func testCursor() { 19 | let r = RecentTextClient("気持ちいい朝です") 20 | let cursor = r.selectedRange() 21 | XCTAssertEqual(8, cursor.location) 22 | XCTAssertEqual(0, cursor.length) 23 | } 24 | func testString() { 25 | let r = RecentTextClient("気持ちいい朝です") 26 | var actual = NSRange(location: NSNotFound, length: NSNotFound) 27 | let string = r.string(from: NSRange(location:6, length:2), actualRange: &actual) 28 | XCTAssertEqual("です", string) 29 | XCTAssertEqual(6, actual.location) 30 | XCTAssertEqual(2, actual.length) 31 | } 32 | func testInsert() { 33 | let r = RecentTextClient("気持ちいい朝です") 34 | r.insertText("なるほどね", replacementRange: NSRange(location: NSNotFound, length: NSNotFound)) 35 | XCTAssertEqual("気持ちいい朝ですなるほどね", r.text) 36 | } 37 | func testInsertReplace() { 38 | let r = RecentTextClient("気持ちいい朝です") 39 | let rr = NSRange(location: 3, length: 3) 40 | r.insertText("なるほどね", replacementRange: rr) 41 | XCTAssertEqual("気持ちなるほどねです", r.text) 42 | } 43 | func testReplaceLast() { 44 | let r = RecentTextClient("気持ちいい朝です") 45 | r.replaceLast(length: 2, with: "だよ") 46 | XCTAssertEqual("気持ちいい朝だよ", r.text) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /MacTcodeTests/TranslationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TranslationTests.swift 3 | // MacTcodeTests 4 | // 5 | // Created by maeda on 2024/06/01. 6 | // 7 | 8 | import XCTest 9 | import Cocoa 10 | import InputMethodKit 11 | @testable import MacTcode 12 | 13 | final class TranslationTests: XCTestCase { 14 | var mode: TcodeMode! 15 | var spy: RecentTextClient! 16 | var client: ContextClient! 17 | 18 | class HolderSpy: Controller { 19 | var mode: Mode 20 | var candidateWindow: IMKCandidates = IMKCandidates() // dummy 21 | init(mode: Mode) { 22 | self.mode = mode 23 | } 24 | func pushMode(_ mode: Mode) { 25 | self.mode = mode 26 | } 27 | func popMode() { 28 | } 29 | } 30 | 31 | func stubCharEvent(_ char: String) -> InputEvent { 32 | return Translator.translate(event: NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, characters: char, charactersIgnoringModifiers: char, isARepeat: false, keyCode: 0)!) 33 | } 34 | func stubCodeEvent(_ code: Int, char: String = "a") -> InputEvent { 35 | return Translator.translate(event: NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, characters: char, charactersIgnoringModifiers: char, isARepeat: false, keyCode: UInt16(code))!) 36 | } 37 | func feed(_ sequence: String) { 38 | sequence.forEach { char in 39 | let event = stubCharEvent(String(char)) 40 | let ret = mode.handle(event, client: client, controller: HolderSpy(mode: mode)) 41 | XCTAssertTrue(ret) 42 | } 43 | } 44 | 45 | override func setUpWithError() throws { 46 | super.setUp() 47 | spy = RecentTextClient("") 48 | client = ContextClient(client: spy, recent: RecentTextClient("")) 49 | mode = TcodeMode() 50 | Log.i("setUp!") 51 | } 52 | 53 | override func tearDownWithError() throws { 54 | mode = nil 55 | super.tearDown() 56 | } 57 | 58 | func testPassthrough() { 59 | let event = stubCharEvent("A") 60 | let ret = mode.handle(event, client: client, controller: HolderSpy(mode: mode)) 61 | XCTAssertFalse(ret) 62 | } 63 | func testSendFirstBySpace() { 64 | feed(" a ") 65 | XCTAssertEqual(" a ", spy.text) 66 | } 67 | func testSendFirstBySpace2() { 68 | feed("\\\\ ") 69 | XCTAssertEqual("\\\\", spy.text) 70 | } 71 | func testInput() { 72 | feed("tesoteso") 73 | XCTAssertEqual("のがのが", spy.text) 74 | } 75 | func testBushu() { 76 | feed("tpkuhus.djue") 77 | XCTAssertEqual("晴れだね", spy.text) 78 | } 79 | func testMazeAutoKakutei() { 80 | feed("zbtphizawvfe.ouhtid;hpto") 81 | XCTAssertEqual("今日は調査に行った", spy.text) 82 | } 83 | func testOutset() { 84 | feed("fqyg\\iqioy") 85 | XCTAssertEqual("全角 空白", spy.text) 86 | } 87 | 88 | func testOutset2() { 89 | feed("se\\\\dsu\\\\t") 90 | XCTAssertEqual("1①2③", spy.text) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /MacTcodeTests/ZenkakuModeTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZenkakuModeTest.swift 3 | // MacTcodeTests 4 | // 5 | // Created by maeda on 2024/06/05. 6 | // 7 | 8 | import XCTest 9 | import Cocoa 10 | import InputMethodKit 11 | @testable import MacTcode 12 | 13 | final class ZenkakuModeTest: XCTestCase { 14 | var mode: Mode! 15 | var spy: RecentTextClient! 16 | var client: ContextClient! 17 | var holder: HolderSpy! 18 | 19 | class HolderSpy: Controller { 20 | var modeStack: [Mode] 21 | var candidateWindow: IMKCandidates = IMKCandidates() 22 | init(mode: Mode) { 23 | self.modeStack = [mode] 24 | } 25 | func pushMode(_ mode: Mode) { 26 | modeStack = [mode] + modeStack 27 | } 28 | func popMode() { 29 | modeStack.removeFirst() 30 | } 31 | var mode: Mode { get { modeStack.first! } } 32 | } 33 | 34 | func stubCharEvent(_ char: String) -> InputEvent { 35 | return Translator.translate(event: NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, characters: char, charactersIgnoringModifiers: char, isARepeat: false, keyCode: 0)!) 36 | } 37 | func stubCodeEvent(_ code: Int, char: String = "a") -> InputEvent { 38 | return Translator.translate(event: NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: 0, context: nil, characters: char, charactersIgnoringModifiers: char, isARepeat: false, keyCode: UInt16(code))!) 39 | } 40 | func feed(_ sequence: String) { 41 | sequence.forEach { char in 42 | let event = stubCharEvent(String(char)) 43 | let ret = holder.mode.handle(event, client: client, controller: holder) 44 | XCTAssertTrue(ret) 45 | } 46 | } 47 | 48 | override func setUpWithError() throws { 49 | super.setUp() 50 | spy = RecentTextClient("", 99) 51 | client = ContextClient(client: spy, recent: RecentTextClient("")) 52 | mode = ZenkakuMode() 53 | holder = HolderSpy(mode: mode) 54 | Log.i("setUp!") 55 | } 56 | 57 | override func tearDownWithError() throws { 58 | mode = nil 59 | super.tearDown() 60 | } 61 | 62 | func testHan2Zen() { 63 | let str = ZenkakuMode().han2zen(" Zenkaku~!") 64 | XCTAssertEqual(" Zenkaku ̄!", str) 65 | } 66 | 67 | func testHan2ZenModeChange() { 68 | holder.pushMode(TcodeMode()) 69 | feed("teso90123ABC#*zenkaku\u{1b}x y z fudefe") 70 | XCTAssertEqual("のが123ABC#*zenkakuxyzあいう", spy.text) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /MacTcodeUITests/MacTcodeUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacTcodeUITests.swift 3 | // MacTcodeUITests 4 | // 5 | // Created by maeda on 2024/05/25. 6 | // 7 | 8 | import XCTest 9 | 10 | final class MacTcodeUITests: XCTestCase { 11 | /* 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 20 | } 21 | 22 | override func tearDownWithError() throws { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | } 25 | 26 | func testExample() throws { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | */ 43 | } 44 | -------------------------------------------------------------------------------- /MacTcodeUITests/MacTcodeUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacTcodeUITestsLaunchTests.swift 3 | // MacTcodeUITests 4 | // 5 | // Created by maeda on 2024/05/25. 6 | // 7 | 8 | import XCTest 9 | 10 | final class MacTcodeUITestsLaunchTests: XCTestCase { 11 | /* 12 | 13 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 14 | true 15 | } 16 | 17 | override func setUpWithError() throws { 18 | continueAfterFailure = false 19 | } 20 | 21 | func testLaunch() throws { 22 | let app = XCUIApplication() 23 | app.launch() 24 | 25 | // Insert steps here to perform after app launch but before taking a screenshot, 26 | // such as logging into a test account or navigating somewhere in the app 27 | 28 | let attachment = XCTAttachment(screenshot: app.screenshot()) 29 | attachment.name = "Launch Screen" 30 | attachment.lifetime = .keepAlways 31 | add(attachment) 32 | } 33 | */ 34 | } 35 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARCDIR=build/archive.xcarchive 2 | ARCDIR_RELEASE=build/archive.release.xcarchive 3 | APPNAME=MacTcode.app 4 | DMGNAME=MacTcode.dmg 5 | WORKDIR=work 6 | SIGNING_IDENTITY="Developer ID Application: Kaoru Maeda (8H7RHH924X)" 7 | BUNDLE_ID=jp.mad-p.inputmethod.MacTcode 8 | 9 | .PHONY: build releaseBuild reload releaseReload sign dmg notary 10 | 11 | build: 12 | xcodebuild -workspace MacTcode.xcodeproj/project.xcworkspace -scheme MacTcode clean archive -archivePath $(ARCDIR) OTHER_SWIFT_FLAGS='-D ENABLE_NSLOG' 13 | 14 | releaseBuild $(WORKDIR)/$(APPNAME): 15 | rm -rf $(WORKDIR)/$(APPNAME) 16 | CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO xcodebuild -workspace MacTcode.xcodeproj/project.xcworkspace -scheme MacTcode clean archive -archivePath $(ARCDIR_RELEASE) -configuration Release -destination 'generic/platform=macOS' 17 | cp -r $(ARCDIR_RELEASE)/Products/Applications/$(APPNAME) $(WORKDIR) 18 | 19 | reload: build 20 | pkill "MacTcode" || true 21 | sudo rm -rf /Library/Input\ Methods/$(APPNAME) 22 | sudo cp -r $(ARCDIR)/Products/Applications/$(APPNAME) /Library/Input\ Methods/ 23 | mkdir -p $(WORKDIR) 24 | 25 | releaseReload: releaseBuild 26 | pkill "MacTcode" || true 27 | sudo rm -rf /Library/Input\ Methods/$(APPNAME) 28 | sudo cp -r $(ARCDIR_RELEASE)/Products/Applications/$(APPNAME) /Library/Input\ Methods/ 29 | 30 | sign: $(WORKDIR)/$(APPNAME) 31 | codesign --deep --force --verify --verbose \ 32 | --sign $(SIGNING_IDENTITY) $(WORKDIR)/$(APPNAME) \ 33 | --options runtime \ 34 | --entitlements MacTcode/MacTcode.entitlements \ 35 | --timestamp 36 | 37 | dmg $(WORKDIR)/$(DMGNAME): sign 38 | hdiutil create -volname "MacTcode" -srcfolder $(WORKDIR)/$(APPNAME) -ov -format UDZO $(WORKDIR)/$(DMGNAME) 39 | codesign --sign $(SIGNING_IDENTITY) --timestamp --verbose $(WORKDIR)/$(DMGNAME) 40 | 41 | notary: dmg 42 | xcrun notarytool submit $(WORKDIR)/$(DMGNAME) --keychain-profile "MacTcode" --wait 43 | xcrun stapler staple $(WORKDIR)/$(DMGNAME) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MacTcode 2 | 3 | ## デモ 4 | 5 | ![](mactcode-demo.gif) 6 | 7 | ## 動機 8 | 9 | macOS用のT-Codeが使えるIMとして[MacUIM](https://github.com/e-kato/macuim)を使っていましたが、永らく更新されていません。 10 | ソースコードは公開されているものの、自分でビルドするのは成功していません。saryとかの用意が難しく…(Ruby 1.9.3をSonomaに入れるとか難しい)。 11 | 12 | そこで、単純なT-Codeだけのドライバなら作っちゃえばいいんじゃね? ということで始めました。 13 | 14 | ## 実装したい機能 15 | 16 | おおむね優先度順 17 | - [x] 基本文字の入力 18 | - [x] postfix部首変換 19 | - [x] postfix交ぜ書き変換 20 | - [x] 変換候補選択画面 21 | - [x] 送りがなサポート 22 | - [x] 全角入力モード (v0.1.4.1) 23 | - [x] 配布パッケージ、notarization 24 | - [ ] configファイルサポート 25 | - [ ] パスワード入力時はパススルーする 26 | - [ ] メニュー(config再読み込みとかテンプレ生成とか) 27 | - [ ] 1行入力(T-Code変換をしつつバッファにため、一気に入力するモード) 28 | - [ ] 3ストローク以上の基本文字サポート 29 | - [ ] 仮想鍵盤 30 | 31 | ## 現状についての警告 32 | 33 | - デバッグビルドでは、NSLogに全ストロークが出力されます。プライバシー注意! 34 | 35 | ## インストール 36 | 37 | - GitHubのリリースタグにdmgがある場合は、開いて以下を実施してください 38 | - `sudo cp -r MacTcode.app /Library/Input\ Methods/` 39 | - アップデートの場合はまず消してから 40 | - dmgがない場合はごめんなさい 41 | - ログアウトしてログインし直すと、入力ソースとして選択できるようになります 42 | - 一部アプリに対して部首交換、交ぜ書き変換する場合、バックスペースを送るためにアクセシビリティ機能を使っています 43 | - システム設定 → プライバシーとセキュリティー → アクセシビリティ でMacTcodeに制御を許可してください 44 | 45 | ## 参考文献 46 | 47 | - https://github.com/ensan-hcl/azooKey-Desktop 48 | - https://github.com/google/mozc/ 49 | - [azooKey on macOSの開発知見](https://zenn.dev/azookey/articles/d06b4ee8039ba9) 50 | - [日本語入力を作るときに必要だった本](https://mzp.booth.pm/items/809262) 51 | - [Typut](https://github.com/ensan-hcl/Typut) 52 | 53 | ## ライセンス 54 | 55 | MIT 56 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/logo.png -------------------------------------------------------------------------------- /logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/logo.xcf -------------------------------------------------------------------------------- /mactcode-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/mactcode-demo.gif -------------------------------------------------------------------------------- /modeicon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mad-p/MacTcode/086c43ba4b8efa4e3ca5b6829268f24098f8230d/modeicon.xcf -------------------------------------------------------------------------------- /reload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # ビルド 4 | xcodebuild -workspace MacTcode.xcodeproj/project.xcworkspace -scheme MacTcode clean archive -archivePath build/archive.xcarchive 5 | # 上書き 6 | sudo rm -rf /Library/Input\ Methods/MacTcode.app 7 | sudo cp -r build/archive.xcarchive/Products/Applications/MacTcode.app /Library/Input\ Methods/ 8 | # 再起動 9 | pkill "MacTcode" 10 | --------------------------------------------------------------------------------