├── PHTV ├── Resources │ ├── donate.png │ ├── icon.png │ ├── menubar_icon.png │ ├── Setup │ │ ├── step2-open.png │ │ ├── step1-download.png │ │ ├── step5-complete.png │ │ ├── step3-permissions.png │ │ └── step4-grant-access.png │ ├── UI │ │ ├── menu-charset.png │ │ ├── settings-macros.png │ │ ├── settings-system.png │ │ ├── settings-typing.png │ │ └── menu-input-methods.png │ ├── menubar_english.png │ └── menubar_vietnamese.png ├── Assets.xcassets │ ├── Contents.json │ ├── donate.imageset │ │ ├── donate.png │ │ └── Contents.json │ ├── menubar_icon.imageset │ │ ├── menubar_icon.png │ │ └── Contents.json │ ├── menubar_english.imageset │ │ ├── menubar_english.png │ │ └── Contents.json │ └── menubar_vietnamese.imageset │ │ ├── menubar_vietnamese.png │ │ └── Contents.json ├── Application │ ├── main.m │ └── AppDelegate.h ├── SwiftUI │ ├── Views │ │ ├── Settings │ │ │ ├── update.json │ │ │ ├── HotkeySettingsView.swift │ │ │ ├── AdvancedSettingsView.swift │ │ │ ├── ThemeSettingsView.swift │ │ │ └── AboutView.swift │ │ ├── Components │ │ │ ├── PermissionWarningView.swift │ │ │ └── MacroEditorView.swift │ │ └── StatusBarMenuView.swift │ ├── Utilities │ │ ├── ThemeManager.swift │ │ ├── BeepManager.swift │ │ ├── ReloadHelpers.swift │ │ ├── SettingsObserver.swift │ │ ├── ColorExtension.swift │ │ ├── HotReloadManager.swift │ │ ├── BackendSyncOptimizer.swift │ │ ├── PreviewHelpers.swift │ │ └── ViewModifiers.swift │ ├── Bridge │ │ └── AppDelegate+SwiftUI.swift │ ├── Components │ │ └── SettingsSliderRow.swift │ └── Controllers │ │ ├── WindowController.swift │ │ └── StatusBarController.swift ├── Views │ ├── MyTextField.m │ └── MyTextField.h ├── Utils │ ├── UsageStats.h │ ├── MJAccessibilityUtils.h │ ├── MJAccessibilityUtils.m │ ├── PHTVUtilities.h │ ├── UsageStats.m │ └── PHTVUtilities.m ├── PHTV-Bridging-Header.h ├── Core │ ├── Engine │ │ ├── ConvertTool.h │ │ ├── SmartSwitchKey.h │ │ ├── Vietnamese.h │ │ ├── Macro.h │ │ ├── SmartSwitchKey.cpp │ │ ├── DataType.h │ │ ├── ConvertTool.cpp │ │ └── Engine.h │ ├── PHTVConfig.h │ ├── Platforms │ │ └── mac.h │ ├── PHTVConstants.h │ └── PHTVConfig.m ├── Managers │ └── PHTVManager.h └── Info.plist ├── PHTV.icon ├── icon.json └── Assets │ └── 1289679474.svg ├── SwiftUIConfig.xcconfig ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── pull_request_template.md ├── .gitignore ├── SECURITY.md ├── CODE_OF_CONDUCT.md ├── PHTV.xcodeproj └── xcshareddata │ └── xcschemes │ └── PHTV.xcscheme ├── INSTALL.md ├── CONTRIBUTING.md ├── FAQ.md ├── README.md ├── LICENSE └── PHTVFunctionalTests.swift /PHTV/Resources/donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/donate.png -------------------------------------------------------------------------------- /PHTV/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/icon.png -------------------------------------------------------------------------------- /PHTV/Resources/menubar_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/menubar_icon.png -------------------------------------------------------------------------------- /PHTV/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PHTV/Resources/Setup/step2-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/Setup/step2-open.png -------------------------------------------------------------------------------- /PHTV/Resources/UI/menu-charset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/UI/menu-charset.png -------------------------------------------------------------------------------- /PHTV/Resources/menubar_english.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/menubar_english.png -------------------------------------------------------------------------------- /PHTV/Resources/UI/settings-macros.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/UI/settings-macros.png -------------------------------------------------------------------------------- /PHTV/Resources/UI/settings-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/UI/settings-system.png -------------------------------------------------------------------------------- /PHTV/Resources/UI/settings-typing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/UI/settings-typing.png -------------------------------------------------------------------------------- /PHTV/Resources/menubar_vietnamese.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/menubar_vietnamese.png -------------------------------------------------------------------------------- /PHTV/Resources/Setup/step1-download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/Setup/step1-download.png -------------------------------------------------------------------------------- /PHTV/Resources/Setup/step5-complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/Setup/step5-complete.png -------------------------------------------------------------------------------- /PHTV/Resources/UI/menu-input-methods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/UI/menu-input-methods.png -------------------------------------------------------------------------------- /PHTV/Resources/Setup/step3-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/Setup/step3-permissions.png -------------------------------------------------------------------------------- /PHTV/Resources/Setup/step4-grant-access.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Resources/Setup/step4-grant-access.png -------------------------------------------------------------------------------- /PHTV/Assets.xcassets/donate.imageset/donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Assets.xcassets/donate.imageset/donate.png -------------------------------------------------------------------------------- /PHTV/Assets.xcassets/menubar_icon.imageset/menubar_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Assets.xcassets/menubar_icon.imageset/menubar_icon.png -------------------------------------------------------------------------------- /PHTV/Assets.xcassets/menubar_english.imageset/menubar_english.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Assets.xcassets/menubar_english.imageset/menubar_english.png -------------------------------------------------------------------------------- /PHTV/Assets.xcassets/menubar_vietnamese.imageset/menubar_vietnamese.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PhamHungTien/PHTV/HEAD/PHTV/Assets.xcassets/menubar_vietnamese.imageset/menubar_vietnamese.png -------------------------------------------------------------------------------- /PHTV/Application/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) { 12 | return NSApplicationMain(argc, argv); 13 | } 14 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Views/Settings/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "Chỉ cập nhật latestVersion khi có phiên bản mới thực sự được release trên GitHub. File này là fallback cho update check.", 3 | "latestVersion": "1.1.8", 4 | "downloadURL": "https://github.com/PhamHungTien/PHTV/releases/latest", 5 | "message": "Có phiên bản mới. Bạn muốn tải xuống?" 6 | } 7 | -------------------------------------------------------------------------------- /PHTV/Assets.xcassets/menubar_icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "menubar_icon.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PHTV/Assets.xcassets/menubar_english.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "menubar_english.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PHTV/Assets.xcassets/menubar_vietnamese.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "menubar_vietnamese.png", 5 | "idiom" : "mac", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "scale" : "2x" 11 | } 12 | ], 13 | "info" : { 14 | "author" : "xcode", 15 | "version" : 1 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /PHTV/Views/MyTextField.m: -------------------------------------------------------------------------------- 1 | // 2 | // MyTextField.m 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | // DEPRECATED: This file is legacy and no longer used. 9 | // All UI components are now implemented in SwiftUI. 10 | 11 | #import "MyTextField.h" 12 | 13 | @implementation MyTextField 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /PHTV/Assets.xcassets/donate.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "donate.png", 5 | "idiom": "universal", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "universal", 10 | "scale": "2x" 11 | }, 12 | { 13 | "idiom": "universal", 14 | "scale": "3x" 15 | } 16 | ], 17 | "info": { 18 | "author": "xcode", 19 | "version": 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /PHTV/Views/MyTextField.h: -------------------------------------------------------------------------------- 1 | // 2 | // MyTextField.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | // DEPRECATED: This file is legacy and no longer used. 9 | // All UI components are now implemented in SwiftUI. 10 | 11 | #import 12 | 13 | // This class is deprecated and kept only for backwards compatibility 14 | @interface MyTextField : NSTextField 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /PHTV/Utils/UsageStats.h: -------------------------------------------------------------------------------- 1 | // 2 | // UsageStats.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface UsageStats : NSObject 14 | 15 | + (instancetype)shared; 16 | - (void)incrementWordCount; 17 | - (void)incrementCharacterCount; 18 | - (NSInteger)getTotalWords; 19 | - (NSInteger)getTotalCharacters; 20 | - (NSInteger)getTodayWords; 21 | - (NSInteger)getTodayCharacters; 22 | - (void)resetDailyStats; 23 | - (NSDictionary *)getStatsSummary; 24 | 25 | @end 26 | 27 | NS_ASSUME_NONNULL_END 28 | -------------------------------------------------------------------------------- /PHTV/Utils/MJAccessibilityUtils.h: -------------------------------------------------------------------------------- 1 | // 2 | // MJAccessibilityUtils.h 3 | // PHTV 4 | // 5 | // Modified by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | // Source: https://github.com/Hammerspoon/hammerspoon/blob/master/Hammerspoon/MJAccessibilityUtils.h 9 | // License: MIT 10 | 11 | #ifndef MJAccessibilityUtils_h 12 | 13 | #import 14 | 15 | #ifdef __cplusplus 16 | extern "C" { 17 | #endif 18 | BOOL MJAccessibilityIsEnabled(void); 19 | void MJAccessibilityOpenPanel(void); 20 | #ifdef __cplusplus 21 | } 22 | #endif 23 | 24 | #define MJAccessibilityUtils_h 25 | 26 | 27 | #endif /* MJAccessibilityUtils_h */ 28 | -------------------------------------------------------------------------------- /PHTV/PHTV-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // PHTV-Bridging-Header.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #ifndef PHTV_Bridging_Header_h 10 | #define PHTV_Bridging_Header_h 11 | 12 | #import 13 | #import 14 | 15 | // Application 16 | #import "Application/AppDelegate.h" 17 | 18 | // Managers 19 | #import "Managers/PHTVManager.h" 20 | 21 | // Utils 22 | #import "Utils/MJAccessibilityUtils.h" 23 | #import "Utils/UsageStats.h" 24 | #import "Utils/PHTVUtilities.h" 25 | 26 | // Core 27 | #import "Core/PHTVConfig.h" 28 | #import "Core/PHTVConstants.h" 29 | 30 | #endif /* PHTV_Bridging_Header_h */ 31 | -------------------------------------------------------------------------------- /PHTV/Core/Engine/ConvertTool.h: -------------------------------------------------------------------------------- 1 | // 2 | // ConvertTool.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #ifndef ConvertTool_h 10 | #define ConvertTool_h 11 | 12 | #include "DataType.h" 13 | #include 14 | using namespace std; 15 | 16 | extern bool convertToolDontAlertWhenCompleted; 17 | extern bool convertToolToAllCaps; 18 | extern bool convertToolToAllNonCaps; 19 | extern bool convertToolToCapsFirstLetter; 20 | extern bool convertToolToCapsEachWord; 21 | extern bool convertToolRemoveMark; 22 | extern Uint8 convertToolFromCode; 23 | extern Uint8 convertToolToCode; 24 | extern int convertToolHotKey; 25 | 26 | string convertUtil(const string& sourceString); 27 | 28 | #endif /* ConvertTool_h */ 29 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Utilities/ThemeManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeManager.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | @MainActor 12 | class ThemeManager: ObservableObject { 13 | static let shared = ThemeManager() 14 | 15 | @AppStorage("themeColor") private var storedColor = AppStorageColor(color: .blue) 16 | 17 | var themeColor: Color { 18 | get { storedColor.color } 19 | set { 20 | objectWillChange.send() 21 | storedColor = AppStorageColor(color: newValue) 22 | } 23 | } 24 | 25 | private init() {} 26 | 27 | // Predefined theme colors 28 | let predefinedColors: [ThemeColor] = Color.themeColors 29 | } 30 | -------------------------------------------------------------------------------- /PHTV.icon/icon.json: -------------------------------------------------------------------------------- 1 | { 2 | "fill" : { 3 | "solid" : "display-p3:0.64482,0.00626,0.00626,1.00000" 4 | }, 5 | "groups" : [ 6 | { 7 | "layers" : [ 8 | { 9 | "image-name" : "1289679474.svg", 10 | "name" : "1289679474", 11 | "position" : { 12 | "scale" : 4, 13 | "translation-in-points" : [ 14 | 0, 15 | 0 16 | ] 17 | } 18 | } 19 | ], 20 | "shadow" : { 21 | "kind" : "neutral", 22 | "opacity" : 0.5 23 | }, 24 | "translucency" : { 25 | "enabled" : true, 26 | "value" : 0.5 27 | } 28 | } 29 | ], 30 | "supported-platforms" : { 31 | "circles" : [ 32 | "watchOS" 33 | ], 34 | "squares" : "shared" 35 | } 36 | } -------------------------------------------------------------------------------- /SwiftUIConfig.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIConfig.xcconfig 3 | // PHTV 4 | // 5 | // Build configuration for SwiftUI support 6 | // 7 | 8 | // Swift Language Version 9 | SWIFT_VERSION = 5.0 10 | 11 | // Bridging Header 12 | SWIFT_OBJC_BRIDGING_HEADER = $(SRCROOT)/PHTV/PHTV-Bridging-Header.h 13 | 14 | // Embed Swift Standard Libraries 15 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 16 | 17 | // Swift Optimization 18 | SWIFT_OPTIMIZATION_LEVEL = -O 19 | 20 | // Swift Compilation Mode 21 | SWIFT_COMPILATION_MODE = wholemodule 22 | 23 | // Enable Swift in Objective-C 24 | CLANG_ENABLE_MODULES = YES 25 | 26 | // Module Name 27 | PRODUCT_MODULE_NAME = PHTV 28 | 29 | // Swift Include Paths 30 | SWIFT_INCLUDE_PATHS = $(inherited) $(SRCROOT)/PHTV 31 | 32 | // Deployment Target (SwiftUI requires 10.15+) 33 | MACOSX_DEPLOYMENT_TARGET = 14.0 34 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Utilities/BeepManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BeepManager.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | @MainActor 12 | final class BeepManager { 13 | static let shared = BeepManager() 14 | 15 | private let sound: NSSound? 16 | 17 | private init() { 18 | self.sound = NSSound(named: NSSound.Name("Pop")) 19 | self.sound?.loops = false 20 | } 21 | 22 | func play(volume: Double) { 23 | let v = max(0.0, min(1.0, volume)) 24 | guard v > 0.0 else { return } 25 | if let sound = self.sound { 26 | sound.stop() 27 | sound.volume = Float(v) 28 | sound.play() 29 | return 30 | } 31 | NSBeep() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Utilities/ReloadHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReloadHelpers.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Helper struct for macro reload notifications 12 | struct MacroReloadHelper { 13 | /// Notify backend about macro reload 14 | static func notifyMacroUpdate(_ macros: T) { 15 | NotificationCenter.default.post( 16 | name: NSNotification.Name("MacrosUpdated"), 17 | object: macros 18 | ) 19 | } 20 | } 21 | 22 | /// Helper struct for excluded apps reload notifications 23 | struct ExcludedAppReloadHelper { 24 | /// Notify backend about excluded apps update 25 | static func notifyExcludedAppsUpdate(_ apps: T) { 26 | NotificationCenter.default.post( 27 | name: NSNotification.Name("ExcludedAppsChanged"), 28 | object: apps 29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PHTV/Core/Engine/SmartSwitchKey.h: -------------------------------------------------------------------------------- 1 | // 2 | // SmartSwitchKey.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #ifndef SmartSwitchKey_h 10 | #define SmartSwitchKey_h 11 | 12 | #include "DataType.h" 13 | #include 14 | 15 | using namespace std; 16 | 17 | void initSmartSwitchKey(const Byte* pData, const int& size); 18 | 19 | /** 20 | * convert all data to save on disk 21 | */ 22 | void getSmartSwitchKeySaveData(vector& outData); 23 | 24 | /** 25 | * find and get language input method, if don't has set @currentInputMethod value for this app 26 | * return: 27 | * -1: don't have this bundleId 28 | * 0: English 29 | * 1: Vietnamese 30 | */ 31 | int getAppInputMethodStatus(const string& bundleId, const int& currentInputMethod); 32 | 33 | /** 34 | * Set default language for this @bundleId 35 | */ 36 | void setAppInputMethodStatus(const string& bundleId, const int& language); 37 | 38 | #endif /* SmartSwitchKey_h */ 39 | -------------------------------------------------------------------------------- /PHTV/Core/Engine/Vietnamese.h: -------------------------------------------------------------------------------- 1 | // 2 | // Vietnamese.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #ifndef Vietnamese_h 10 | #define Vietnamese_h 11 | #include "DataType.h" 12 | #include 13 | #include 14 | 15 | using namespace std; 16 | 17 | extern Uint16 douKey[2][2]; 18 | 19 | extern map>> _vowel; 20 | extern map>> _vowelCombine; 21 | extern map>> _vowelForMark; 22 | extern vector> _consonantD; 23 | extern vector> _consonantTable; 24 | extern vector> _endConsonantTable; 25 | extern vector _standaloneWbad; 26 | extern vector> _doubleWAllowed; 27 | 28 | extern map> _codeTable[]; 29 | extern Uint16 _unicodeCompoundMark[]; 30 | 31 | extern map> _quickTelex; 32 | extern map> _quickStartConsonant; 33 | extern map> _quickEndConsonant; 34 | extern map _characterMap; 35 | 36 | extern Uint16 keyCodeToCharacter(const Uint32& keyCode); 37 | #endif /* Vietnamese_h */ 38 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Views/Settings/HotkeySettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HotkeySettingsView.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct HotkeySettingsView: View { 12 | @EnvironmentObject var appState: AppState 13 | @EnvironmentObject var themeManager: ThemeManager 14 | 15 | var body: some View { 16 | ScrollView { 17 | VStack(spacing: 20) { 18 | // Hotkey Configuration 19 | SettingsCard(title: "Phím tắt chuyển chế độ", icon: "command.circle.fill") { 20 | HotkeyConfigView() 21 | } 22 | 23 | // Pause Key Configuration 24 | SettingsCard(title: "Tạm dừng gõ tiếng Việt", icon: "pause.circle.fill") { 25 | PauseKeyConfigView() 26 | } 27 | 28 | Spacer(minLength: 20) 29 | } 30 | .padding(20) 31 | } 32 | .background(Color(NSColor.windowBackgroundColor)) 33 | } 34 | } 35 | 36 | #Preview { 37 | HotkeySettingsView() 38 | .environmentObject(AppState.shared) 39 | .frame(width: 500, height: 600) 40 | } 41 | -------------------------------------------------------------------------------- /PHTV/Managers/PHTVManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // PHTVManager.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #ifndef PHTVManager_h 10 | #define PHTVManager_h 11 | 12 | #import 13 | #import 14 | 15 | @interface PHTVManager : NSObject 16 | 17 | // Core functionality 18 | +(BOOL)isInited; 19 | +(BOOL)initEventTap; 20 | +(BOOL)stopEventTap; 21 | +(void)handleEventTapDisabled:(CGEventType)type; 22 | +(BOOL)isEventTapEnabled; 23 | +(void)ensureEventTapAlive; 24 | 25 | // CRITICAL: Permission loss protection 26 | +(BOOL)hasPermissionLost; 27 | +(void)markPermissionLost; 28 | 29 | // SAFE permission check via test event tap (Apple recommended approach) 30 | +(BOOL)canCreateEventTap; 31 | +(void)invalidatePermissionCache; // Force fresh check on next call 32 | 33 | // Table codes 34 | +(NSArray*)getTableCodes; 35 | 36 | // Utilities 37 | +(NSString*)getBuildDate; 38 | +(void)showMessage:(NSWindow*)window message:(NSString*)msg subMsg:(NSString*)subMsg; 39 | 40 | // Convert feature 41 | +(BOOL)quickConvert; 42 | 43 | // Application Support 44 | +(NSString*)getApplicationSupportFolder; 45 | 46 | @end 47 | 48 | #endif /* PHTVManager_h */ 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request / Đề xuất Tính năng 3 | about: Đề xuất tính năng mới cho PHTV 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | 13 | 14 | ## 📋 Vấn đề hay Request (Problem or Request) 15 | 16 | 17 | 18 | [Mô tả ở đây] 19 | 20 | ## 💡 Giải pháp được đề xuất (Proposed Solution) 21 | 22 | 23 | 24 | [Mô tả giải pháp ở đây] 25 | 26 | ## 🔄 Các giải pháp thay thế (Alternative Solutions) 27 | 28 | 29 | 30 | [Mô tả giải pháp thay thế ở đây, hoặc xóa nếu không có] 31 | 32 | ## 📚 Ngữ cảnh bổ sung (Additional Context) 33 | 34 | 35 | 36 | [Thêm ngữ cảnh ở đây] 37 | 38 | ## 🎯 Mức độ ưu tiên (Priority) 39 | 40 | - [ ] 🔴 Rất cao (Critical) - Ảnh hưởng đến hầu hết người dùng 41 | - [ ] 🟠 Cao (High) - Ảnh hưởng đến nhiều người dùng 42 | - [ ] 🟡 Trung bình (Medium) - Tính năng hữu ích 43 | - [ ] 🟢 Thấp (Low) - Cải thiện nhỏ 44 | 45 | --- 46 | 47 | **Cảm ơn vì đã đóng góp ý tưởng!** 💭 48 | -------------------------------------------------------------------------------- /PHTV/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | PHTV 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIconFile 12 | PHTV 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(MARKETING_VERSION) 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSApplicationCategoryType 26 | public.app-category.utilities 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | LSMultipleInstancesProhibited 30 | 31 | NSAppleEventsUsageDescription 32 | Used for keyboard 33 | NSHumanReadableCopyright 34 | Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 35 | NSPrincipalClass 36 | NSApplication 37 | 38 | 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode build products 2 | build/ 3 | *.pbxuser 4 | *.mode1v3 5 | *.mode2v3 6 | *.perspectivev3 7 | *.xcworkspace/xcuserdata/ 8 | xcuserdata/ 9 | *.xccheckout 10 | *.moved-aside 11 | DerivedData/ 12 | *.hmap 13 | *.ipa 14 | *.xcarchive 15 | *.xcodeproj/xcuserdata 16 | *.app 17 | 18 | # Swift Package Manager 19 | .build/ 20 | .swiftpm/ 21 | *.resolved 22 | Package.resolved 23 | 24 | # CocoaPods 25 | Pods/ 26 | Podfile.lock 27 | *.xcworkspace/ 28 | 29 | # Carthage 30 | Carthage/ 31 | Checkouts/ 32 | 33 | # fastlane 34 | fastlane/report.xml 35 | fastlane/Preview.html 36 | fastlane/screenshots/**/*.png 37 | fastlane/test_output 38 | 39 | # Accio dependency management 40 | Dependencies/ 41 | .accio/ 42 | 43 | # Code coverage 44 | *.coverage 45 | *.profraw 46 | 47 | # macOS 48 | .DS_Store 49 | .AppleDouble 50 | .LSOverride 51 | ._* 52 | .Spotlight-V100 53 | .Trashes 54 | 55 | # IDE 56 | .vscode/ 57 | .idea/ 58 | *.swp 59 | *.swo 60 | *~ 61 | .project 62 | .settings/ 63 | .classpath 64 | 65 | # Build artifacts 66 | *.o 67 | *.out 68 | *.a 69 | *.dylib 70 | 71 | # Python 72 | __pycache__/ 73 | *.py[cod] 74 | *$py.class 75 | .Python 76 | *.egg-info/ 77 | *.egg 78 | 79 | # Runtime 80 | *.so 81 | *.dll 82 | 83 | # Logs 84 | *.log 85 | npm-debug.log* 86 | yarn-debug.log* 87 | yarn-error.log* 88 | 89 | # Environment 90 | .env 91 | .env.local 92 | .env.*.local 93 | 94 | # Temporary files 95 | *~ 96 | *.bak 97 | *.tmp 98 | *.swp 99 | .DS_Store 100 | 101 | # Misc 102 | .claude/ 103 | .git/ 104 | .gitkeep 105 | -------------------------------------------------------------------------------- /PHTV/Utils/MJAccessibilityUtils.m: -------------------------------------------------------------------------------- 1 | // 2 | // MJAccessibilityUtils.m 3 | // PHTV 4 | // 5 | // Modified by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | // Source: https://github.com/Hammerspoon/hammerspoon/blob/master/Hammerspoon/MJAccessibilityUtils.m 9 | // License: MIT 10 | 11 | 12 | #import "MJAccessibilityUtils.h" 13 | // #import "HSLogger.h" 14 | 15 | extern Boolean AXAPIEnabled(void); 16 | extern Boolean AXIsProcessTrustedWithOptions(CFDictionaryRef options) __attribute__((weak_import)); 17 | extern CFStringRef kAXTrustedCheckOptionPrompt __attribute__((weak_import)); 18 | 19 | 20 | BOOL MJAccessibilityIsEnabled(void) { 21 | BOOL isEnabled = NO; 22 | if (AXIsProcessTrustedWithOptions != NULL) 23 | isEnabled = AXIsProcessTrustedWithOptions(NULL); 24 | else 25 | #pragma clang diagnostic push 26 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 27 | isEnabled = AXAPIEnabled(); 28 | #pragma clang diagnostic pop 29 | 30 | // HSNSLOG(@"Accessibility is: %@", isEnabled ? @"ENABLED" : @"DISABLED"); 31 | return isEnabled; 32 | } 33 | 34 | void MJAccessibilityOpenPanel(void) { 35 | if (AXIsProcessTrustedWithOptions != NULL) { 36 | AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)@{(__bridge id)kAXTrustedCheckOptionPrompt: @YES}); 37 | } 38 | else { 39 | static NSString* script = @"tell application \"System Preferences\"\nactivate\nset current pane to pane \"com.apple.preference.universalaccess\"\nend tell"; 40 | [[[NSAppleScript alloc] initWithSource:script] executeAndReturnError:nil]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PHTV/Application/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | #define PHTV_BUNDLE @"com.phamhungtien.phtv" 14 | 15 | @interface AppDelegate : NSObject 16 | 17 | -(void)onImputMethodChanged:(BOOL)willNotify; 18 | -(void)onInputMethodSelected; 19 | 20 | -(void)askPermission; 21 | 22 | -(void)onInputTypeSelectedIndex:(int)index; 23 | -(void)onCodeTableChanged:(int)index; 24 | -(void)fillData; // Update UI status bar and menu items 25 | 26 | -(void)setRunOnStartup:(BOOL)val; 27 | -(void)loadDefaultConfig; 28 | 29 | -(void)setGrayIcon:(BOOL)val; 30 | 31 | -(void)onMacroSelected; 32 | -(void)onQuickConvert; 33 | -(void)setQuickConvertString; 34 | 35 | -(void)showIconOnDock:(BOOL)val; 36 | -(void)showIcon:(BOOL)onDock; 37 | 38 | // Accessibility monitoring methods 39 | - (void)startAccessibilityMonitoring; 40 | - (void)stopAccessibilityMonitoring; 41 | - (void)checkAccessibilityStatus; 42 | - (void)handleAccessibilityRevoked; 43 | - (void)setupSwiftUIBridge; 44 | - (void)loadExistingMacros; 45 | - (void)handleCheckForUpdates:(NSNotification * _Nullable)notification; 46 | - (void)handleSettingsReset:(NSNotification * _Nullable)notification; 47 | 48 | @end 49 | 50 | NS_ASSUME_NONNULL_END 51 | 52 | #ifdef __cplusplus 53 | extern "C" { 54 | #endif 55 | 56 | // Global function to get AppDelegate instance 57 | AppDelegate* _Nullable GetAppDelegateInstance(void); 58 | 59 | #ifdef __cplusplus 60 | } 61 | #endif 62 | 63 | 64 | -------------------------------------------------------------------------------- /PHTV/Utils/PHTVUtilities.h: -------------------------------------------------------------------------------- 1 | // 2 | // PHTVUtilities.h 3 | // PHTV - Vietnamese Input Method 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | // High-performance utility functions 9 | // 10 | 11 | #ifndef PHTVUtilities_h 12 | #define PHTVUtilities_h 13 | 14 | #import 15 | #include "PHTVConstants.h" 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | /** 20 | * @brief Optimized utility functions for PHTV 21 | * @author Phạm Hùng Tiến 22 | */ 23 | @interface PHTVUtilities : NSObject 24 | 25 | #pragma mark - String Processing (Optimized) 26 | + (NSString *)formatNumber:(NSNumber *)number; 27 | + (NSString *)buildDateString; 28 | + (NSString *)versionString; 29 | 30 | #pragma mark - Key Processing 31 | + (BOOL)isVowelKey:(int)keyCode; 32 | + (BOOL)isConsonantKey:(int)keyCode; 33 | + (BOOL)isMarkKey:(int)keyCode inputType:(PHTVInputType)type; 34 | + (BOOL)isToneKey:(int)keyCode inputType:(PHTVInputType)type; 35 | 36 | #pragma mark - Character Conversion 37 | + (unichar)removeVietnameseTone:(unichar)character; 38 | + (unichar)addTone:(unichar)character toneType:(int)tone; 39 | + (NSString *)normalizeVietnameseString:(NSString *)input; 40 | 41 | #pragma mark - Performance Utilities 42 | + (void)executeOnMainThread:(dispatch_block_t)block; 43 | + (void)executeOnBackgroundThread:(dispatch_block_t)block; 44 | + (void)executeAfterDelay:(NSTimeInterval)delay block:(dispatch_block_t)block; 45 | 46 | #pragma mark - Cache Management 47 | + (void)clearCache; 48 | + (NSUInteger)cacheSize; 49 | 50 | @end 51 | 52 | NS_ASSUME_NONNULL_END 53 | 54 | #endif /* PHTVUtilities_h */ 55 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Utilities/SettingsObserver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsObserver.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | /// Monitors UserDefaults changes and notifies AppState in real-time 13 | @MainActor 14 | final class SettingsObserver: NSObject, ObservableObject { 15 | static let shared = SettingsObserver() 16 | 17 | @Published var settingsDidChange: Date? 18 | 19 | nonisolated(unsafe) private var observer: NSObjectProtocol? 20 | 21 | private var lastNotificationTime: Date = Date() 22 | 23 | private override init() { 24 | super.init() 25 | setupObserver() 26 | } 27 | 28 | private func setupObserver() { 29 | // Listen on the main queue so @MainActor isolation is respected 30 | observer = NotificationCenter.default.addObserver( 31 | forName: UserDefaults.didChangeNotification, 32 | object: UserDefaults.standard, 33 | queue: .main 34 | ) { [weak self] _ in 35 | Task { @MainActor in 36 | self?.didChangeSettings() 37 | } 38 | } 39 | } 40 | 41 | private func didChangeSettings() { 42 | // Debounce notifications to avoid excessive updates 43 | let now = Date() 44 | if now.timeIntervalSince(lastNotificationTime) > 0.1 { 45 | lastNotificationTime = now 46 | Task { @MainActor in 47 | self.settingsDidChange = now 48 | } 49 | } 50 | } 51 | 52 | deinit { 53 | if let observer { 54 | NotificationCenter.default.removeObserver(observer) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /PHTV/Core/Engine/Macro.h: -------------------------------------------------------------------------------- 1 | // 2 | // Macro.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #ifndef Macro_h 10 | #define Macro_h 11 | 12 | #include 13 | #include 14 | #include 15 | #include "DataType.h" 16 | 17 | using namespace std; 18 | 19 | struct MacroData { 20 | string macroText; //ex: "ms" 21 | string macroContent; //ex: "millisecond" 22 | vector macroContentCode; //converted of macroContent 23 | }; 24 | 25 | /** 26 | * Call when you need to load macro data from disk 27 | */ 28 | extern "C" { 29 | void initMacroMap(const Byte* pData, const int& size); 30 | } 31 | 32 | /** 33 | * convert all macro data to save on disk 34 | */ 35 | void getMacroSaveData(vector& outData); 36 | 37 | /** 38 | * Use to find full text by macro 39 | */ 40 | bool findMacro(vector& key, vector& macroContentCode); 41 | 42 | /** 43 | * check has this macro or not 44 | */ 45 | bool hasMacro(const string& macroName); 46 | 47 | /** 48 | * Get all macro to show on macro table 49 | */ 50 | void getAllMacro(vector>& keys, vector& macroTexts, vector& macroContents); 51 | 52 | /** 53 | * add new macro to memory 54 | */ 55 | bool addMacro(const string& macroText, const string& macroContent); 56 | 57 | /** 58 | * delete macro from memory 59 | */ 60 | bool deleteMacro(const string& macroText); 61 | 62 | /** 63 | * When table code changed, we have to call this function to reload all macroContentCode 64 | */ 65 | void onTableCodeChange(); 66 | 67 | /** 68 | * Save all macro data to disk 69 | */ 70 | void saveToFile(const string& path); 71 | 72 | /** 73 | * Load macro data from disk 74 | */ 75 | void readFromFile(const string& path, const bool& append=true); 76 | 77 | #endif /* Macro_h */ 78 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## 📝 Mô tả (Description) 7 | 8 | 9 | 10 | [Mô tả thay đổi của bạn] 11 | 12 | ## 🔗 Liên kết đến Issue (Linked Issue) 13 | 14 | 15 | 16 | Closes #[issue number] 17 | 18 | ## ✅ Loại thay đổi (Type of Change) 19 | 20 | - [ ] 🐛 Bug fix - sửa lỗi không ảnh hưởng đến API 21 | - [ ] ✨ New feature - tính năng mới 22 | - [ ] 💥 Breaking change - thay đổi có ảnh hưởng 23 | - [ ] 📚 Documentation update - cập nhật tài liệu 24 | - [ ] 🎨 Style update - thay đổi format/style 25 | - [ ] ♻️ Refactoring - tái cấu trúc code 26 | 27 | ## 🧪 Testing 28 | 29 | 30 | 31 | - [ ] Tested locally 32 | - [ ] Tested on macOS 14.x 33 | - [ ] Tested on macOS 15.x 34 | - [ ] Tested in dark mode 35 | - [ ] Tested with excluded apps 36 | - [ ] Tested with multiple input methods 37 | 38 | **Cách tái hiện test:** 39 | 40 | ``` 41 | 1. Bước 1 42 | 2. Bước 2 43 | ... 44 | ``` 45 | 46 | ## 📋 Checklist 47 | 48 | - [ ] Tôi đã đọc [CONTRIBUTING.md](../CONTRIBUTING.md) 49 | - [ ] Tôi đã cập nhật code để tuân theo [code style guidelines](../CONTRIBUTING.md#quy-tắc-code) 50 | - [ ] Tôi đã cập nhật documentation nếu cần 51 | - [ ] Tôi đã cập nhật CHANGELOG.md 52 | - [ ] Các thay đổi của tôi không tạo ra warning 53 | - [ ] Tôi đã test thay đổi của tôi thoroughly 54 | - [ ] Tôi không thêm dependencies không cần thiết 55 | 56 | ## 📸 Screenshots (nếu có liên quan) 57 | 58 | 59 | 60 | ## 🎯 Lưu ý cho Reviewer 61 | 62 | 63 | 64 | --- 65 | 66 | ## 💭 Self-Review 67 | 68 | 69 | 70 | - [ ] Tôi đã kiểm tra code của tôi và không tìm thấy vấn đề 71 | - [ ] Tôi đã comment code của tôi ở các vị trí khó hiểu 72 | - [ ] Tôi đã cập nhật documentation liên quan 73 | - [ ] Tôi đã xóa code dư thừa/debugging statements 74 | - [ ] Tôi đã kiểm tra performance impact 75 | 76 | --- 77 | 78 | **Cảm ơn vì đóng góp!** 🎉 79 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Utilities/ColorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorExtension.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | // MARK: - AppStorageColor Wrapper 12 | 13 | struct AppStorageColor: RawRepresentable { 14 | var color: Color 15 | 16 | init(color: Color) { 17 | self.color = color 18 | } 19 | 20 | init?(rawValue: String) { 21 | guard let data = Data(base64Encoded: rawValue) else { 22 | return nil 23 | } 24 | 25 | do { 26 | let nsColor = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: data) 27 | guard let color = nsColor else { 28 | return nil 29 | } 30 | self.color = Color(nsColor: color) 31 | } catch { 32 | return nil 33 | } 34 | } 35 | 36 | var rawValue: String { 37 | do { 38 | let nsColor = NSColor(color) 39 | let data = try NSKeyedArchiver.archivedData(withRootObject: nsColor, requiringSecureCoding: false) 40 | return data.base64EncodedString() 41 | } catch { 42 | return "" 43 | } 44 | } 45 | } 46 | 47 | // MARK: - Predefined Theme Colors 48 | 49 | extension Color { 50 | static let themeColors: [ThemeColor] = [ 51 | ThemeColor(id: "blue", name: "Xanh dương", color: .blue), 52 | ThemeColor(id: "purple", name: "Tím", color: .purple), 53 | ThemeColor(id: "pink", name: "Hồng", color: .pink), 54 | ThemeColor(id: "red", name: "Đỏ", color: .red), 55 | ThemeColor(id: "orange", name: "Cam", color: .orange), 56 | ThemeColor(id: "yellow", name: "Vàng", color: .yellow), 57 | ThemeColor(id: "green", name: "Xanh lá", color: .green), 58 | ThemeColor(id: "teal", name: "Xanh ngọc", color: .teal), 59 | ThemeColor(id: "indigo", name: "Chàm", color: .indigo), 60 | ThemeColor(id: "mint", name: "Bạc hà", color: .mint), 61 | ThemeColor(id: "cyan", name: "Lục lam", color: .cyan), 62 | ThemeColor(id: "brown", name: "Nâu", color: .brown), 63 | ] 64 | } 65 | 66 | // MARK: - Theme Color Model 67 | 68 | struct ThemeColor: Identifiable { 69 | let id: String 70 | let name: String 71 | let color: Color 72 | } 73 | -------------------------------------------------------------------------------- /PHTV/Core/Engine/SmartSwitchKey.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // SmartSwitchKey.cpp 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #include "SmartSwitchKey.h" 10 | #include 11 | #include 12 | #include 13 | 14 | //main data, i use `map` because it has O(Log(n)) 15 | static map _smartSwitchKeyData; 16 | static string _cacheKey = ""; //use cache for faster 17 | static Int8 _cacheData = 0; //use cache for faster 18 | 19 | void initSmartSwitchKey(const Byte* pData, const int& size) { 20 | _smartSwitchKeyData.clear(); 21 | if (pData == NULL) return; 22 | Uint16 count = 0; 23 | Uint32 cursor = 0; 24 | if (size >= 2) { 25 | memcpy(&count, pData + cursor, 2); 26 | cursor+=2; 27 | } 28 | Uint8 bundleIdSize; 29 | Uint8 value; 30 | for (int i = 0; i < count; i++) { 31 | bundleIdSize = pData[cursor++]; 32 | string bundleId((char*)pData + cursor, bundleIdSize); 33 | cursor += bundleIdSize; 34 | value = pData[cursor++]; 35 | _smartSwitchKeyData[bundleId] = value; 36 | } 37 | } 38 | 39 | void getSmartSwitchKeySaveData(vector& outData) { 40 | outData.clear(); 41 | Uint16 count = (Uint16)_smartSwitchKeyData.size(); 42 | outData.push_back((Byte)count); 43 | outData.push_back((Byte)(count>>8)); 44 | 45 | for (std::map::iterator it = _smartSwitchKeyData.begin(); it != _smartSwitchKeyData.end(); ++it) { 46 | outData.push_back((Byte)it->first.length()); 47 | for (int j = 0; j < it->first.length(); j++) { 48 | outData.push_back(it->first[j]); 49 | } 50 | outData.push_back(it->second); 51 | } 52 | } 53 | 54 | int getAppInputMethodStatus(const string& bundleId, const int& currentInputMethod) { 55 | if (_cacheKey.compare(bundleId) == 0) { 56 | return _cacheData; 57 | } 58 | if (_smartSwitchKeyData.find(bundleId) != _smartSwitchKeyData.end()) { 59 | _cacheKey = bundleId; 60 | _cacheData = _smartSwitchKeyData[bundleId]; 61 | return _cacheData; 62 | } 63 | _cacheKey = bundleId; 64 | _cacheData = currentInputMethod; 65 | _smartSwitchKeyData[bundleId] = _cacheData; 66 | return -1; 67 | } 68 | 69 | void setAppInputMethodStatus(const string& bundleId, const int& language) { 70 | _smartSwitchKeyData[bundleId] = language; 71 | _cacheKey = bundleId; 72 | _cacheData = language; 73 | } 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report / Báo cáo Lỗi 3 | about: Báo cáo lỗi để giúp chúng tôi cải thiện PHTV 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | 13 | 14 | ## 📝 Mô tả lỗi (Bug Description) 15 | 16 | 17 | 18 | [Mô tả lỗi ở đây] 19 | 20 | ## 🔄 Cách tái hiện (Steps to Reproduce) 21 | 22 | 23 | 24 | 1. [Bước 1] 25 | 2. [Bước 2] 26 | 3. [Bước 3] 27 | ... 28 | 29 | ## 📸 Hành vi mong đợi (Expected Behavior) 30 | 31 | 32 | 33 | [Mô tả hành vi mong đợi] 34 | 35 | ## 🎬 Hành vi thực tế (Actual Behavior) 36 | 37 | 38 | 39 | [Mô tả hành vi thực tế] 40 | 41 | ## 📸 Ảnh chụp màn hình / Video (Screenshots/Videos) 42 | 43 | 44 | 45 | [Thêm ảnh/video ở đây hoặc xóa phần này nếu không cần] 46 | 47 | ## 💻 Thông tin hệ thống (System Information) 48 | 49 | **macOS Version:** 50 | 51 | - [ ] macOS 15.x (Sequoia) 52 | - [ ] macOS 14.x (Sonoma) 53 | - [ ] Other: **\_\_\_** 54 | 55 | **PHTV Version:** 56 | 57 | 58 | 59 | **Xcode Version (nếu build từ source):** 60 | 61 | 62 | 63 | **Ứng dụng bị ảnh hưởng:** 64 | 65 | 66 | 67 | ## 🔧 Điều gì bạn đã cố gắng? (What have you tried?) 68 | 69 | 70 | 71 | - [ ] Khởi động lại PHTV 72 | - [ ] Khởi động lại macOS 73 | - [ ] Vô hiệu hóa/Bật lại Accessibility permission 74 | - [ ] Other: **\_\_\_** 75 | 76 | ## 📊 Thông tin bổ sung (Additional Context) 77 | 78 | 79 | 80 | --- 81 | 82 | **Mức độ nghiêm trọng (Severity):** 83 | 84 | - [ ] 🔴 Lỗi nghiêm trọng (Critical) - Ứng dụng bị crash hoặc không thể sử dụng 85 | - [ ] 🟠 Lỗi cao (High) - Tính năng chính không hoạt động 86 | - [ ] 🟡 Lỗi trung bình (Medium) - Tính năng không hoạt động đúng 87 | - [ ] 🟢 Lỗi thấp (Low) - Vấn đề nhỏ hoặc cosmetic 88 | 89 | **Tần suất (Frequency):** 90 | 91 | - [ ] Lúc nào cũng xảy ra (Always) 92 | - [ ] Thường xuyên (Frequently) 93 | - [ ] Thỉnh thoảng (Occasionally) 94 | - [ ] Hiếm khi (Rarely) 95 | 96 | --- 97 | 98 | **Cảm ơn vì đã giúp chúng tôi!** 🙏 99 | -------------------------------------------------------------------------------- /PHTV/Core/PHTVConfig.h: -------------------------------------------------------------------------------- 1 | // 2 | // PHTVConfig.h 3 | // PHTV - Vietnamese Input Method 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | // Runtime Configuration Manager - Singleton Pattern 9 | // 10 | 11 | #ifndef PHTVConfig_h 12 | #define PHTVConfig_h 13 | 14 | #import 15 | #include "PHTVConstants.h" 16 | 17 | NS_ASSUME_NONNULL_BEGIN 18 | 19 | /** 20 | * @brief Centralized configuration manager for PHTV 21 | * @author Phạm Hùng Tiến 22 | * 23 | * This class manages all runtime configurations and preferences 24 | * using a singleton pattern for thread-safe access across the application. 25 | */ 26 | @interface PHTVConfig : NSObject 27 | 28 | #pragma mark - Singleton 29 | + (instancetype)shared; 30 | 31 | #pragma mark - Core Settings 32 | @property (nonatomic, assign) PHTVInputMethod inputMethod; 33 | @property (nonatomic, assign) PHTVInputType inputType; 34 | @property (nonatomic, assign) PHTVCodeTable codeTable; 35 | 36 | #pragma mark - Feature Toggles 37 | @property (nonatomic, assign) BOOL freeMarkEnabled; 38 | @property (nonatomic, assign) BOOL modernOrthographyEnabled; 39 | @property (nonatomic, assign) BOOL spellCheckEnabled; 40 | @property (nonatomic, assign) BOOL quickTelexEnabled; 41 | @property (nonatomic, assign) BOOL restoreOnInvalidWordEnabled; 42 | @property (nonatomic, assign) BOOL fixBrowserRecommendEnabled; 43 | @property (nonatomic, assign) BOOL macroEnabled; 44 | @property (nonatomic, assign) BOOL macroInEnglishModeEnabled; 45 | @property (nonatomic, assign) BOOL smartSwitchKeyEnabled; 46 | @property (nonatomic, assign) BOOL upperCaseFirstCharEnabled; 47 | @property (nonatomic, assign) BOOL tempDisableSpellCheckEnabled; 48 | @property (nonatomic, assign) BOOL allowConsonantZFWJEnabled; 49 | @property (nonatomic, assign) BOOL quickStartConsonantEnabled; 50 | @property (nonatomic, assign) BOOL quickEndConsonantEnabled; 51 | @property (nonatomic, assign) BOOL rememberCodeTableEnabled; 52 | @property (nonatomic, assign) BOOL autoCapsMacroEnabled; 53 | @property (nonatomic, assign) BOOL sendKeyStepByStepEnabled; 54 | @property (nonatomic, assign) BOOL fixChromiumBrowserEnabled; 55 | @property (nonatomic, assign) BOOL performLayoutCompatEnabled; 56 | @property (nonatomic, assign) BOOL tempDisablePHTVEnabled; 57 | @property (nonatomic, assign) BOOL otherLanguageDetectionEnabled; 58 | 59 | #pragma mark - UI Settings 60 | @property (nonatomic, assign) BOOL grayIconEnabled; 61 | @property (nonatomic, assign) BOOL showIconOnDockEnabled; 62 | @property (nonatomic, assign) BOOL showUIOnStartupEnabled; 63 | @property (nonatomic, assign) BOOL runOnStartupEnabled; 64 | 65 | #pragma mark - Advanced Settings 66 | @property (nonatomic, assign) int switchKeyStatus; 67 | 68 | #pragma mark - Methods 69 | - (void)loadFromUserDefaults; 70 | - (void)saveToUserDefaults; 71 | - (void)resetToDefaults; 72 | - (NSDictionary *)exportSettings; 73 | - (void)importSettings:(NSDictionary *)settings; 74 | 75 | @end 76 | 77 | NS_ASSUME_NONNULL_END 78 | 79 | #endif /* PHTVConfig_h */ 80 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Utilities/HotReloadManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HotReloadManager.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | /// Manages hot-reload of app data without requiring restart 13 | @MainActor 14 | final class HotReloadManager: NSObject, ObservableObject { 15 | static let shared = HotReloadManager() 16 | 17 | @Published var macrosUpdated: Date? 18 | @Published var excludedAppsUpdated: Date? 19 | @Published var settingsUpdated: Date? 20 | 21 | private var settingsObserver: NSKeyValueObservation? 22 | private var debounceTimer: Timer? 23 | 24 | private override init() { 25 | super.init() 26 | setupHotReload() 27 | } 28 | 29 | private func setupHotReload() { 30 | // Monitor UserDefaults for macro changes 31 | NotificationCenter.default.addObserver( 32 | self, 33 | selector: #selector(handleMacroChanges), 34 | name: NSNotification.Name("MacrosUpdated"), 35 | object: nil 36 | ) 37 | 38 | // Monitor excluded apps changes 39 | NotificationCenter.default.addObserver( 40 | self, 41 | selector: #selector(handleExcludedAppsChanges), 42 | name: NSNotification.Name("ExcludedAppsChanged"), 43 | object: nil 44 | ) 45 | 46 | // Monitor settings changes 47 | NotificationCenter.default.addObserver( 48 | self, 49 | selector: #selector(handleSettingsChanges), 50 | name: NSNotification.Name("PHTVSettingsChanged"), 51 | object: nil 52 | ) 53 | } 54 | 55 | @objc private func handleMacroChanges() { 56 | Task { @MainActor in 57 | self.macrosUpdated = Date() 58 | // Notify backend to reload macros 59 | NotificationCenter.default.post( 60 | name: NSNotification.Name("MacroDataNeedsReload"), 61 | object: nil 62 | ) 63 | } 64 | } 65 | 66 | @objc private func handleExcludedAppsChanges() { 67 | Task { @MainActor in 68 | self.excludedAppsUpdated = Date() 69 | // Notify backend to reload excluded apps 70 | NotificationCenter.default.post( 71 | name: NSNotification.Name("ExcludedAppsNeedsReload"), 72 | object: nil 73 | ) 74 | } 75 | } 76 | 77 | @objc private func handleSettingsChanges() { 78 | Task { @MainActor in 79 | self.settingsUpdated = Date() 80 | } 81 | } 82 | 83 | /// Force reload all data without restarting 84 | func reloadAll() { 85 | NotificationCenter.default.post( 86 | name: NSNotification.Name("ReloadAllData"), 87 | object: nil 88 | ) 89 | macrosUpdated = Date() 90 | excludedAppsUpdated = Date() 91 | settingsUpdated = Date() 92 | } 93 | 94 | deinit { 95 | NotificationCenter.default.removeObserver(self) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Bridge/AppDelegate+SwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate+SwiftUI.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AppKit 11 | 12 | // Extension to bridge SwiftUI with existing Objective-C AppDelegate 13 | extension AppDelegate { 14 | 15 | @objc func setupSwiftUIBridge() { 16 | // Subscribe to notifications from SwiftUI 17 | NotificationCenter.default.addObserver( 18 | self, 19 | selector: #selector(handleInputMethodChanged(_:)), 20 | name: NSNotification.Name("InputMethodChanged"), 21 | object: nil 22 | ) 23 | 24 | NotificationCenter.default.addObserver( 25 | self, 26 | selector: #selector(handleCodeTableChanged(_:)), 27 | name: NSNotification.Name("CodeTableChanged"), 28 | object: nil 29 | ) 30 | 31 | NotificationCenter.default.addObserver( 32 | self, 33 | selector: #selector(handleToggleEnabled(_:)), 34 | name: NSNotification.Name("ToggleEnabled"), 35 | object: nil 36 | ) 37 | 38 | NotificationCenter.default.addObserver( 39 | self, 40 | selector: #selector(handleShowConvertTool), 41 | name: NSNotification.Name("ShowConvertTool"), 42 | object: nil 43 | ) 44 | 45 | NotificationCenter.default.addObserver( 46 | self, 47 | selector: #selector(handleShowAbout), 48 | name: NSNotification.Name("ShowAbout"), 49 | object: nil 50 | ) 51 | } 52 | 53 | @objc private func handleInputMethodChanged(_ notification: Notification) { 54 | guard let inputMethod = notification.object as? Int else { return } 55 | // Update existing vInputType variable 56 | // This will be implemented to call existing methods 57 | print("Input method changed to: \(inputMethod)") 58 | } 59 | 60 | @objc private func handleCodeTableChanged(_ notification: Notification) { 61 | guard let codeTable = notification.object as? Int else { return } 62 | // Update existing vCodeTable variable 63 | print("Code table changed to: \(codeTable)") 64 | } 65 | 66 | @objc private func handleToggleEnabled(_ notification: Notification) { 67 | guard let enabled = notification.object as? Bool else { return } 68 | // Toggle PHTV on/off 69 | print("PHTV enabled: \(enabled)") 70 | } 71 | 72 | @objc private func handleShowConvertTool() { 73 | // Show convert tool window 74 | // Call existing method to show convert tool 75 | } 76 | 77 | @objc private func handleShowAbout() { 78 | // Show about window 79 | // Call existing method to show about 80 | } 81 | 82 | // Sync state to SwiftUI 83 | @objc func syncStateToSwiftUI() { 84 | DispatchQueue.main.async { 85 | _ = AppState.shared 86 | // Sync current state from Objective-C to SwiftUI 87 | // This will be called when settings change in Objective-C side 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Components/SettingsSliderRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsSliderRow.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SettingsSliderRow: View { 12 | let icon: String 13 | let iconColor: Color 14 | let title: String 15 | let subtitle: String 16 | let minValue: Double 17 | let maxValue: Double 18 | let step: Double 19 | @Binding var value: Double 20 | var valueFormatter: (Double) -> String = { String(format: "%.0f", $0) } 21 | 22 | var body: some View { 23 | VStack(spacing: 12) { 24 | HStack(spacing: 14) { 25 | ZStack { 26 | if #available(macOS 26.0, *) { 27 | RoundedRectangle(cornerRadius: 8) 28 | .fill(iconColor.opacity(0.12)) 29 | .frame(width: 36, height: 36) 30 | .glassEffect(in: .rect(cornerRadius: 8)) 31 | } else { 32 | RoundedRectangle(cornerRadius: 8) 33 | .fill(iconColor.opacity(0.12)) 34 | .frame(width: 36, height: 36) 35 | } 36 | 37 | Image(systemName: icon) 38 | .font(.system(size: 16, weight: .medium)) 39 | .foregroundStyle(iconColor) 40 | } 41 | 42 | VStack(alignment: .leading, spacing: 2) { 43 | Text(title) 44 | .font(.body) 45 | .foregroundStyle(.primary) 46 | 47 | Text(subtitle) 48 | .font(.caption) 49 | .foregroundStyle(.secondary) 50 | } 51 | 52 | Spacer() 53 | 54 | Text(valueFormatter(value)) 55 | .font(.subheadline) 56 | .fontWeight(.semibold) 57 | .foregroundStyle(.tint) 58 | .frame(minWidth: 40, alignment: .trailing) 59 | } 60 | 61 | Slider( 62 | value: $value, 63 | in: minValue...maxValue 64 | ) 65 | .tint(iconColor) 66 | } 67 | .padding(.vertical, 6) 68 | } 69 | } 70 | 71 | #Preview { 72 | VStack(spacing: 16) { 73 | SettingsSliderRow( 74 | icon: "speaker.wave.2.fill", 75 | iconColor: .blue, 76 | title: "Âm lượng beep", 77 | subtitle: "Điều chỉnh mức âm lượng tiếng beep", 78 | minValue: 0.0, 79 | maxValue: 1.0, 80 | step: 0.1, 81 | value: .constant(0.5), 82 | valueFormatter: { String(format: "%.0f%%", $0 * 100) } 83 | ) 84 | 85 | Divider() 86 | .padding(.leading, 50) 87 | 88 | SettingsSliderRow( 89 | icon: "textformat.size", 90 | iconColor: .blue, 91 | title: "Kích cỡ font", 92 | subtitle: "Điều chỉnh kích cỡ chữ hiển thị", 93 | minValue: 8.0, 94 | maxValue: 24.0, 95 | step: 1.0, 96 | value: .constant(14.0), 97 | valueFormatter: { String(format: "%.0f pt", $0) } 98 | ) 99 | } 100 | .padding(16) 101 | } 102 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🔒 Chính sách bảo mật 4 | 5 | **Security Policy - Báo cáo lỗ hổng bảo mật** 6 | 7 | [🏠 Trang chủ](README.md) • [📧 Email](mailto:hungtien10a7@gmail.com) 8 | 9 |
10 | 11 | --- 12 | 13 | ## 🚨 Báo cáo lỗi bảo mật 14 | 15 | **Không mở public issue** cho lỗ hổng bảo mật. 16 | 17 | ### Liên hệ 18 | 19 | Gửi email chi tiết lỗ hổng đến: **hungtien10a7@gmail.com** 20 | 21 | Bao gồm: 22 | 23 | - Mô tả lỗ hổng 24 | - Cách tái hiện 25 | - Tác động tiềm ẩn 26 | - PHTV & macOS version 27 | 28 | ### ⏱️ Timeline 29 | 30 | - **Ngay khi nhận:** Xác nhận báo cáo 31 | - **Trong 48 giờ:** Đánh giá mức độ nghiêm trọng 32 | - **Trong 7 ngày:** Bắt đầu làm việc trên bản vá 33 | - **Trước release vá:** Liên hệ với bạn 34 | - **Sau release:** Công bố lỗ hổng và bản vá 35 | 36 | ### 🤝 Tiết lộ có trách nhiệm 37 | 38 | - **Cho bạn:** Vui lòng cho chúng tôi thời gian bản vá trước khi công bố 39 | - **Cho chúng tôi:** Chúng tôi sẽ bản vá và thông báo người dùng nhanh chóng 40 | 41 | ## 📌 Hỗ trợ phiên bản 42 | 43 | | Phiên bản | Hỗ trợ | 44 | | --------- | ----------------- | 45 | | 1.x | ✅ Đầy đủ | 46 | | 0.x | ⚠️ Quan trọng chỉ | 47 | 48 | ## 📊 Mức độ nghiêm trọng 49 | 50 | ### 🔴 Lỗ hổng nghiêm trọng 51 | 52 | - Remote code execution 53 | - Tấn công elevation of privilege 54 | 55 | **Ví dụ:** Một cách để đọc các tệp người dùng khác trên macOS 56 | 57 | ### 🟠 Lỗ hổng cao 58 | 59 | - Hành vi không mong muốn có thể ảnh hưởng đến bảo mật 60 | - Tính năng bảo mật yếu 61 | 62 | **Ví dụ:** Macro không được xác thực một cách đúng đắn 63 | 64 | ### 🟡 Lỗ hổng trung bình 65 | 66 | - Tính năng không hoạt động như mong đợi 67 | - Tiềm năng bị lạm dụng 68 | 69 | ### 🟢 Lỗ hổng thấp 70 | 71 | - Các vấn đề nhỏ không có tác động bảo mật rõ ràng 72 | 73 | ## ✅ Các thực hành bảo mật tốt 74 | 75 | ### 👤 Để người dùng 76 | 77 | - **Cập nhật thường xuyên:** Cài đặt các bản cập nhật của PHTV ngay khi có sẵn 78 | - **Cấp quyền cẩn thận:** Chỉ cấp quyền Accessibility cho PHTV (đó là cách nó hoạt động) 79 | - **Exclude sensitive apps:** Thêm các ứng dụng nhạy cảm vào danh sách Excluded Apps 80 | - **Theo dõi macro:** Kiểm tra macro được thêm nếu bạn có nghi ngờ 81 | 82 | ### 👨‍💻 Để nhà phát triển 83 | 84 | - Chúng tôi tuân theo các thực hành bảo mật tốt: 85 | - Code review trước merge 86 | - Kiểm tra dependency 87 | - Tránh đọc/ghi file không cần thiết 88 | - Sử dụng HTTPS cho tất cả các yêu cầu mạng (nếu có) 89 | 90 | ## 📢 Công khai lỗ hổng 91 | 92 | Khi chúng tôi công bố lỗ hổng bảo mật, chúng tôi sẽ: 93 | 94 | 1. Phát hành phiên bản mới với bản vá 95 | 2. Cập nhật trang GitHub Releases 96 | 3. Gửi thông báo qua email cho người dùng (nếu có) 97 | 4. Đăng chi tiết trong CHANGELOG 98 | 99 | **Format công bố:** 100 | 101 | ``` 102 | SECURITY: [X.X.X] Bảng vá cho lỗ hổng [tên lỗ hổng] 103 | 104 | Mô tả: [Mô tả chi tiết] 105 | Ảnh hưởng: [Ai bị ảnh hưởng] 106 | Giải pháp: [Cập nhật lên phiên bản mới] 107 | CVE: [Nếu có] 108 | ``` 109 | 110 | ## 📞 Liên hệ 111 | 112 | Nếu bạn có bất kỳ câu hỏi bảo mật nào, vui lòng liên hệ với người duy trì dự án. 113 | 114 | --- 115 | 116 |
117 | 118 | ### 🔒 Bảo mật là ưu tiên hàng đầu 119 | 120 | Chúng tôi cam kết bảo vệ người dùng và xử lý mọi báo cáo bảo mật một cách nghiêm túc. 121 | 122 | [![Security Policy](https://img.shields.io/badge/Security-Policy-red?logo=security)](SECURITY.md) 123 | 124 | **Cảm ơn đã giúp PHTV an toàn hơn!** 125 | 126 | [🏠 Trang chủ](README.md) • [📧 Email bảo mật](mailto:hungtien10a7@gmail.com) 127 | 128 |
129 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Views/Settings/AdvancedSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvancedSettingsView.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct AdvancedSettingsView: View { 12 | @EnvironmentObject var appState: AppState 13 | @EnvironmentObject var themeManager: ThemeManager 14 | 15 | var body: some View { 16 | ScrollView { 17 | VStack(spacing: 20) { 18 | // Advanced Typing Options 19 | SettingsCard(title: "Tùy chọn nâng cao", icon: "gearshape.2.fill") { 20 | VStack(spacing: 0) { 21 | SettingsToggleRow( 22 | icon: "character", 23 | iconColor: themeManager.themeColor, 24 | title: "Phụ âm Z, F, W, J", 25 | subtitle: "Cho phép nhập các phụ âm ngoại lai", 26 | isOn: $appState.allowConsonantZFWJ 27 | ) 28 | 29 | SettingsDivider() 30 | 31 | SettingsToggleRow( 32 | icon: "arrow.right.circle.fill", 33 | iconColor: themeManager.themeColor, 34 | title: "Phụ âm đầu nhanh", 35 | subtitle: "Gõ nhanh phụ âm đầu", 36 | isOn: $appState.quickStartConsonant 37 | ) 38 | 39 | SettingsDivider() 40 | 41 | SettingsToggleRow( 42 | icon: "arrow.left.circle.fill", 43 | iconColor: themeManager.themeColor, 44 | title: "Phụ âm cuối nhanh", 45 | subtitle: "Gõ nhanh phụ âm cuối", 46 | isOn: $appState.quickEndConsonant 47 | ) 48 | 49 | SettingsDivider() 50 | 51 | SettingsToggleRow( 52 | icon: "memorychip.fill", 53 | iconColor: themeManager.themeColor, 54 | title: "Nhớ bảng mã", 55 | subtitle: "Lưu bảng mã khi đóng ứng dụng", 56 | isOn: $appState.rememberCode 57 | ) 58 | } 59 | } 60 | 61 | // Send Key Step By Step 62 | SettingsCard(title: "Gửi từng phím", icon: "keyboard.badge.ellipsis") { 63 | VStack(spacing: 0) { 64 | SettingsToggleRow( 65 | icon: "keyboard.badge.ellipsis", 66 | iconColor: themeManager.themeColor, 67 | title: "Bật gửi từng phím", 68 | subtitle: "Gửi từng ký tự một (chậm nhưng ổn định)", 69 | isOn: $appState.sendKeyStepByStep 70 | ) 71 | } 72 | } 73 | 74 | // Send Key Step By Step Apps 75 | SettingsCard(title: "Ứng dụng gửi từng phím", icon: "app.badge.fill") { 76 | SendKeyStepByStepAppsView() 77 | } 78 | 79 | Spacer(minLength: 20) 80 | } 81 | .padding(20) 82 | } 83 | .background(Color(NSColor.windowBackgroundColor)) 84 | } 85 | } 86 | 87 | #Preview { 88 | AdvancedSettingsView() 89 | .environmentObject(AppState.shared) 90 | .frame(width: 500, height: 600) 91 | } 92 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 📋 Quy tắc ứng xử 4 | 5 | **Code of Conduct - Tạo môi trường cộng đồng thân thiện** 6 | 7 | [🏠 Trang chủ](README.md) • [🤝 Đóng góp](CONTRIBUTING.md) 8 | 9 |
10 | 11 | --- 12 | 13 | ## 🤝 Cam kết của chúng tôi 14 | 15 | Để tạo ra một môi trường mở và đón tiếp, chúng tôi - những người đóng góp và người duy trì cam kết làm cho việc tham gia vào dự án của chúng tôi và cộng đồng của chúng tôi trở thành một trải nghiệm không bị quấy rối cho tất cả mọi người, bất kể tuổi tác, kích thước cơ thể, khuyết tật, dân tộc, đặc tính giới tính, bản sắc giới tính và biểu hiện, mức độ kinh nghiệm, giáo dục, tình trạng kinh tế xã hội, quốc tịch, diện mạo, chủng tộc, tôn giáo, hoặc bản sắc và định hướng tính dục. 16 | 17 | ## 📏 Tiêu chuẩn của chúng tôi 18 | 19 | Những hành vi góp phần tạo ra môi trường tích cực bao gồm: 20 | 21 | - Sử dụng ngôn ngữ chào đón và bao hàm 22 | - Tôn trọng những quan điểm và kinh nghiệm khác nhau 23 | - Tiếp nhận phê bình xây dựng một cách nhã nhặn 24 | - Tập trung vào điều tốt nhất cho cộng đồng 25 | - Thể hiện sự đồng cảm với các thành viên cộng đồng khác 26 | 27 | Các hành vi không chấp nhận bao gồm: 28 | 29 | - Sử dụng ngôn ngữ hoặc hình ảnh tình dục, gây quấy rối hoặc có tính xúc phạm 30 | - Troll, bình luận xúc phạm, và tấn công cá nhân hoặc chính trị 31 | - Quấy rối công khai hoặc riêng tư 32 | - Xuất bản thông tin cá nhân của người khác, chẳng hạn như địa chỉ vật lý hoặc email, mà không có sự cho phép rõ ràng 33 | - Các hành vi khác mà bạn biết rằng sẽ được coi là không phù hợp trong bối cảnh chuyên nghiệp 34 | 35 | ## ⚖️ Thực thi quy tắc 36 | 37 | Người duy trì dự án chịu trách nhiệm làm rõ các tiêu chuẩn hành vi chấp nhận được và dự kiến sẽ thực hiện hành động sửa chữa thích hợp và công bằng để đáp ứng bất kỳ trường hợp hành vi không chấp nhận được nào. 38 | 39 | Người duy trì dự án có quyền và trách nhiệm xóa, chỉnh sửa hoặc từ chối bình luận, commit, code, chỉnh sửa wiki, vấn đề và các đóng góp khác không tuân thủ Quy tắc ứng xử này hoặc cấm tạm thời hoặc vĩnh viễn bất kỳ người đóng góp nào vì hành vi mà họ coi là không phù hợp, đe dọa, xúc phạm hoặc có hại. 40 | 41 | ## 🌍 Phạm vi áp dụng 42 | 43 | Quy tắc ứng xử này áp dụng: 44 | 45 | - Trong không gian dự án, chẳng hạn như repository, issue tracker, discussion, mailing list 46 | - Trong không gian công cộng khi một cá nhân đang đại diện cho dự án hoặc cộng đồng của nó 47 | 48 | ## 🚨 Thực thi 49 | 50 | Các trường hợp hành vi lạm dụng, quấy rối hoặc không chấp nhận được có thể được báo cáo bằng cách liên hệ với người duy trì dự án qua email. Tất cả các khiếu nại sẽ được xem xét và điều tra, và sẽ dẫn đến phản hồi mà được coi là cần thiết và thích hợp cho hoàn cảnh. 51 | 52 | Tất cả người lãnh đạo dự án có trách nhiệm bảo vệ sự bảo mật của người báo cáo sự cố. 53 | 54 | ## 📖 Quy tắc ứng xử này 55 | 56 | Quy tắc ứng xử này được thích nghi từ [Contributor Covenant][homepage], phiên bản 2.1, có sẵn tại https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. 57 | 58 | --- 59 | 60 | ## 📋 Tóm tắt nhanh 61 | 62 | ✅ **Làm:** 63 | 64 | - Tôn trọng những người khác 65 | - Sử dụng ngôn ngữ lịch sự 66 | - Chấp nhận phê bình xây dựng 67 | - Tập trung vào vấn đề, không phải cá nhân 68 | - Giúp đỡ những người khác 69 | 70 | ❌ **Không làm:** 71 | 72 | - Quấy rối hoặc bắt nạt 73 | - Sử dụng ngôn ngữ xúc phạm 74 | - Tấn công cá nhân 75 | - Chia sẻ thông tin cá nhân của người khác 76 | - Spam hoặc off-topic 77 | 78 | --- 79 | 80 |
81 | 82 | **Cảm ơn đã giúp tạo ra một cộng đồng thân thiện và đón tiếp!** ❤️ 83 | 84 | [🏠 Trang chủ](README.md) • [🤝 Đóng góp](CONTRIBUTING.md) • [📧 Báo cáo vi phạm](mailto:hungtien10a7@gmail.com) 85 | 86 |
87 | -------------------------------------------------------------------------------- /PHTV.xcodeproj/xcshareddata/xcschemes/PHTV.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 76 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Controllers/WindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowController.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AppKit 11 | 12 | /// Window controller for hosting SwiftUI views in NSWindow 13 | class SwiftUIWindowController: NSWindowController, NSWindowDelegate { 14 | 15 | convenience init(rootView: Content, title: String, size: NSSize = NSSize(width: 800, height: 600), unifiedTitlebar: Bool = false) { 16 | var styleMask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable] 17 | 18 | if unifiedTitlebar { 19 | styleMask.insert(.fullSizeContentView) 20 | } 21 | 22 | let window = NSWindow( 23 | contentRect: NSRect(x: 0, y: 0, width: size.width, height: size.height), 24 | styleMask: styleMask, 25 | backing: .buffered, 26 | defer: false 27 | ) 28 | 29 | window.title = title 30 | window.center() 31 | window.contentView = NSHostingView(rootView: rootView) 32 | window.setFrameAutosaveName(title) 33 | 34 | if unifiedTitlebar { 35 | // Completely hide titlebar, content extends to top 36 | window.titlebarAppearsTransparent = true 37 | window.titleVisibility = .hidden 38 | 39 | // Remove the toolbar completely 40 | window.toolbar = nil 41 | 42 | // Make window movable by dragging background 43 | window.isMovableByWindowBackground = true 44 | 45 | // Hide title bar but keep traffic lights 46 | window.standardWindowButton(.closeButton)?.superview?.superview?.isHidden = false 47 | } 48 | 49 | self.init(window: window) 50 | window.delegate = self 51 | } 52 | 53 | func show() { 54 | window?.makeKeyAndOrderFront(nil) 55 | NSApp.activate(ignoringOtherApps: true) 56 | } 57 | 58 | // Handle window close to release reference 59 | func windowWillClose(_ notification: Notification) { 60 | // Restore dock icon state to user preference when closing settings 61 | if window?.title == "Cài đặt PHTV" || window?.title == "Cài đặt" { 62 | let appDelegate = NSApplication.shared.delegate as? AppDelegate 63 | let showDock = UserDefaults.standard.bool(forKey: "vShowIconOnDock") 64 | appDelegate?.showIcon(showDock) 65 | } 66 | } 67 | } 68 | 69 | // MARK: - Convenience Factory Methods 70 | extension SwiftUIWindowController { 71 | 72 | static func settingsWindow() -> SwiftUIWindowController { 73 | let controller = SwiftUIWindowController( 74 | rootView: SettingsView() 75 | .environmentObject(AppState.shared) 76 | .frame(minWidth: 700, minHeight: 500), 77 | title: "Cài đặt PHTV", 78 | size: NSSize(width: 800, height: 600), 79 | unifiedTitlebar: true 80 | ) 81 | return controller 82 | } 83 | 84 | static func aboutWindow() -> SwiftUIWindowController { 85 | let controller = SwiftUIWindowController( 86 | rootView: AboutView() 87 | .environmentObject(AppState.shared), 88 | title: "Về PHTV", 89 | size: NSSize(width: 500, height: 600) 90 | ) 91 | if let mask = controller.window?.styleMask { 92 | var newMask = mask 93 | newMask.remove(.resizable) 94 | controller.window?.styleMask = newMask 95 | } 96 | return controller 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /PHTV/Utils/UsageStats.m: -------------------------------------------------------------------------------- 1 | // 2 | // UsageStats.m 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #import "UsageStats.h" 10 | 11 | #define STATS_TOTAL_WORDS @"StatsTotalWords" 12 | #define STATS_TOTAL_CHARS @"StatsTotalCharacters" 13 | #define STATS_TODAY_WORDS @"StatsTodayWords" 14 | #define STATS_TODAY_CHARS @"StatsTodayCharacters" 15 | #define STATS_LAST_RESET_DATE @"StatsLastResetDate" 16 | 17 | @implementation UsageStats 18 | 19 | + (instancetype)shared { 20 | static UsageStats *instance = nil; 21 | static dispatch_once_t onceToken; 22 | dispatch_once(&onceToken, ^{ 23 | instance = [[self alloc] init]; 24 | [instance checkAndResetDailyStats]; 25 | }); 26 | return instance; 27 | } 28 | 29 | - (instancetype)init { 30 | self = [super init]; 31 | if (self) { 32 | // Check and reset stats daily 33 | [self checkAndResetDailyStats]; 34 | } 35 | return self; 36 | } 37 | 38 | - (void)checkAndResetDailyStats { 39 | NSString *lastResetDate = [[NSUserDefaults standardUserDefaults] stringForKey:STATS_LAST_RESET_DATE]; 40 | NSString *today = [self getTodayDateString]; 41 | 42 | if (!lastResetDate || ![lastResetDate isEqualToString:today]) { 43 | [self resetDailyStats]; 44 | } 45 | } 46 | 47 | - (NSString *)getTodayDateString { 48 | NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 49 | [formatter setDateFormat:@"yyyy-MM-dd"]; 50 | return [formatter stringFromDate:[NSDate date]]; 51 | } 52 | 53 | - (void)incrementWordCount { 54 | NSInteger total = [self getTotalWords]; 55 | NSInteger today = [self getTodayWords]; 56 | 57 | [[NSUserDefaults standardUserDefaults] setInteger:total + 1 forKey:STATS_TOTAL_WORDS]; 58 | [[NSUserDefaults standardUserDefaults] setInteger:today + 1 forKey:STATS_TODAY_WORDS]; 59 | [[NSUserDefaults standardUserDefaults] synchronize]; 60 | } 61 | 62 | - (void)incrementCharacterCount { 63 | NSInteger total = [self getTotalCharacters]; 64 | NSInteger today = [self getTodayCharacters]; 65 | 66 | [[NSUserDefaults standardUserDefaults] setInteger:total + 1 forKey:STATS_TOTAL_CHARS]; 67 | [[NSUserDefaults standardUserDefaults] setInteger:today + 1 forKey:STATS_TODAY_CHARS]; 68 | [[NSUserDefaults standardUserDefaults] synchronize]; 69 | } 70 | 71 | - (NSInteger)getTotalWords { 72 | return [[NSUserDefaults standardUserDefaults] integerForKey:STATS_TOTAL_WORDS]; 73 | } 74 | 75 | - (NSInteger)getTotalCharacters { 76 | return [[NSUserDefaults standardUserDefaults] integerForKey:STATS_TOTAL_CHARS]; 77 | } 78 | 79 | - (NSInteger)getTodayWords { 80 | [self checkAndResetDailyStats]; 81 | return [[NSUserDefaults standardUserDefaults] integerForKey:STATS_TODAY_WORDS]; 82 | } 83 | 84 | - (NSInteger)getTodayCharacters { 85 | [self checkAndResetDailyStats]; 86 | return [[NSUserDefaults standardUserDefaults] integerForKey:STATS_TODAY_CHARS]; 87 | } 88 | 89 | - (void)resetDailyStats { 90 | [[NSUserDefaults standardUserDefaults] setInteger:0 forKey:STATS_TODAY_WORDS]; 91 | [[NSUserDefaults standardUserDefaults] setInteger:0 forKey:STATS_TODAY_CHARS]; 92 | [[NSUserDefaults standardUserDefaults] setObject:[self getTodayDateString] forKey:STATS_LAST_RESET_DATE]; 93 | [[NSUserDefaults standardUserDefaults] synchronize]; 94 | } 95 | 96 | - (NSDictionary *)getStatsSummary { 97 | return @{ 98 | @"totalWords": @([self getTotalWords]), 99 | @"totalCharacters": @([self getTotalCharacters]), 100 | @"todayWords": @([self getTodayWords]), 101 | @"todayCharacters": @([self getTodayCharacters]) 102 | }; 103 | } 104 | 105 | @end 106 | -------------------------------------------------------------------------------- /INSTALL.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 📦 Hướng dẫn cài đặt PHTV 4 | 5 | **Cài đặt bộ gõ tiếng Việt cho macOS trong 3 phút** 6 | 7 | [🏠 Trang chủ](README.md) • [💬 FAQ](FAQ.md) • [🐛 Báo lỗi](../../issues) 8 | 9 |
10 | 11 | --- 12 | 13 | ## 🚀 Tải xuống 14 | 15 | **[👉 Tải PHTV từ phamhungtien.com/PHTV](https://phamhungtien.com/PHTV/)** 16 | 17 | ## 📥 Cách cài đặt 18 | 19 | ### 🖼️ Hướng dẫn có ảnh 20 | 21 |
22 | 23 | **Bước 1: Tải về** 24 | Tải PHTV 25 | 26 | **Bước 2: Mở ứng dụng** 27 | Mở PHTV 28 | 29 | **Bước 3: Yêu cầu quyền** 30 | Yêu cầu Accessibility 31 | 32 | **Bước 4: Cấp quyền Accessibility** 33 | Cấp quyền 34 | 35 | **Bước 5: Hoàn tất** 36 | Hoàn tất cài đặt 37 | 38 |
39 | 40 | --- 41 | 42 | ### 🌐 Option 1: Từ Website (Khuyến khích) 43 | 44 | 1. Tải từ [phamhungtien.com/PHTV](https://phamhungtien.com/PHTV/) 45 | 2. Drag `PHTV.app` vào `Applications` 46 | 3. Khởi động từ Launchpad hoặc Spotlight 47 | 48 | ### 🐙 Option 2: Từ GitHub Releases 49 | 50 | 1. Vào [GitHub Releases](https://github.com/PhamHungTien/PHTV/releases) 51 | 2. Download `PHTV.dmg` 52 | 3. Double-click để mở DMG 53 | 4. Drag `PHTV.app` vào `Applications` 54 | 55 | ### 💻 Option 3: Từ Source Code 56 | 57 | ```bash 58 | # Clone repository 59 | git clone https://github.com/PhamHungTien/PHTV.git 60 | cd PHTV 61 | 62 | # Build với Xcode 63 | xcodebuild -scheme PHTV -configuration Release -arch arm64 -arch x86_64 64 | 65 | # App sẽ được build tại: build/Release/PHTV.app 66 | ``` 67 | 68 | ## ⚙️ Yêu cầu hệ thống 69 | 70 | - **macOS**: 14.0 hoặc cao hơn (Sonoma+) 71 | - **Bộ xử lý**: Apple Silicon (M1/M2/M3) hoặc Intel 72 | - **Dung lượng**: ~50 MB 73 | 74 | ## 🔧 Các bước sau khi cài 75 | 76 | 1. **Cấp quyền Accessibility** - App sẽ yêu cầu lần đầu 77 | 2. **Chọn phương pháp gõ** - Settings → Telex hoặc VNI 78 | 3. **Tùy chỉnh phím chuyển** - Settings → Keyboard Shortcuts (optional) 79 | 4. **Thêm Macros** - Settings → Macros (optional) 80 | 81 | ## 📖 Tài liệu thêm 82 | 83 | - 📚 [Hướng dẫn chi tiết](https://phamhungtien.com/PHTV/#setup) - Video & Screenshots 84 | - ⚡ [Các tính năng](README.md#-tính-năng-nổi-bật) 85 | - 💬 [FAQ](FAQ.md) - Câu hỏi thường gặp 86 | - 🤝 [Đóng góp](CONTRIBUTING.md) 87 | 88 | --- 89 | 90 | ## 🆘 Xử lý sự cố 91 | 92 |
93 | PHTV không hoạt động 94 | 95 | **Kiểm tra:** 96 | 97 | 1. Đảm bảo đã cấp quyền **Accessibility** 98 | 2. Restart PHTV từ menu bar (Quit → Reopen) 99 | 3. Kiểm tra **System Settings > Privacy & Security > Accessibility** 100 | 101 |
102 | 103 |
104 | Không gõ được tiếng Việt 105 | 106 | **Giải pháp:** 107 | 108 | 1. Click icon PHTV trên menu bar 109 | 2. Đảm bảo chọn "**Tiếng Việt**" (không phải English) 110 | 3. Kiểm tra phương pháp gõ (Telex/VNI) 111 | 112 |
113 | 114 |
115 | Phím tắt không hoạt động 116 | 117 | **Kiểm tra:** 118 | 119 | 1. Settings → System → Hotkey Configuration 120 | 2. Đảm bảo không trùng với phím tắt khác trong macOS 121 | 3. Thử đổi sang tổ hợp phím khác 122 | 123 |
124 | 125 | --- 126 | 127 |
128 | 129 | **Vẫn gặp vấn đề?** [Tạo issue trên GitHub](../../issues/new) hoặc [Liên hệ qua email](mailto:hungtien10a7@gmail.com) 130 | 131 | [🏠 Về trang chủ](README.md) • [📧 Email](mailto:hungtien10a7@gmail.com) • [💬 Discussions](../../discussions) 132 | 133 |
134 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Utilities/BackendSyncOptimizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackendSyncOptimizer.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import Combine 10 | import Foundation 11 | 12 | /// Optimizes communication with C++ backend to ensure efficient updates 13 | @MainActor 14 | final class BackendSyncOptimizer: NSObject, ObservableObject { 15 | static let shared = BackendSyncOptimizer() 16 | 17 | private var pendingNotifications: Set = [] 18 | private var syncTimer: Timer? 19 | private let syncDebounceInterval: TimeInterval = 0.15 20 | 21 | private override init() { 22 | super.init() 23 | setupOptimization() 24 | } 25 | 26 | private func setupOptimization() { 27 | NotificationCenter.default.addObserver( 28 | self, 29 | selector: #selector(handleSettingChange), 30 | name: NSNotification.Name("HotkeyChanged"), 31 | object: nil 32 | ) 33 | 34 | NotificationCenter.default.addObserver( 35 | self, 36 | selector: #selector(handleMacroChange), 37 | name: NSNotification.Name("MacrosUpdated"), 38 | object: nil 39 | ) 40 | 41 | NotificationCenter.default.addObserver( 42 | self, 43 | selector: #selector(handleExcludedAppsChange), 44 | name: NSNotification.Name("ExcludedAppsChanged"), 45 | object: nil 46 | ) 47 | } 48 | 49 | @objc private func handleSettingChange() { 50 | scheduleSyncNotification("HotkeySync") 51 | } 52 | 53 | @objc private func handleMacroChange() { 54 | scheduleSyncNotification("MacroSync") 55 | } 56 | 57 | @objc private func handleExcludedAppsChange() { 58 | scheduleSyncNotification("ExcludedAppsSync") 59 | } 60 | 61 | /// Schedule a debounced sync notification 62 | private func scheduleSyncNotification(_ notificationName: String) { 63 | pendingNotifications.insert(notificationName) 64 | 65 | syncTimer?.invalidate() 66 | syncTimer = Timer.scheduledTimer(withTimeInterval: syncDebounceInterval, repeats: false) { 67 | [weak self] _ in 68 | Task { @MainActor in 69 | self?.flushPendingNotifications() 70 | } 71 | } 72 | } 73 | 74 | /// Send all pending notifications to backend at once 75 | private func flushPendingNotifications() { 76 | for notificationName in pendingNotifications { 77 | NotificationCenter.default.post( 78 | name: NSNotification.Name(notificationName), 79 | object: nil 80 | ) 81 | } 82 | pendingNotifications.removeAll() 83 | syncTimer?.invalidate() 84 | syncTimer = nil 85 | } 86 | 87 | /// Force immediate sync without debouncing 88 | func syncImmediate() { 89 | flushPendingNotifications() 90 | } 91 | 92 | deinit { 93 | NotificationCenter.default.removeObserver(self) 94 | syncTimer?.invalidate() 95 | } 96 | } 97 | 98 | /// Performance monitoring for settings operations 99 | struct PerformanceMonitor { 100 | static func measureSettingsSave(_ name: String, block: () -> T) -> T { 101 | let start = Date() 102 | let result = block() 103 | let elapsed = Date().timeIntervalSince(start) 104 | 105 | // Only log if it takes longer than 50ms 106 | if elapsed > 0.05 { 107 | NSLog("⚠️ [PHTV] \(name) took \(String(format: "%.0f", elapsed * 1000))ms") 108 | } 109 | 110 | return result 111 | } 112 | 113 | static func logSettings(_ name: String, settings: [String: Any]) { 114 | NSLog("📊 [PHTV] \(name): \(settings.count) settings saved") 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Views/Components/PermissionWarningView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PermissionWarningView.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PermissionWarningView: View { 12 | let hasPermission: Bool 13 | 14 | var body: some View { 15 | if hasPermission { 16 | // Success state 17 | HStack(spacing: 12) { 18 | Image(systemName: "checkmark.shield.fill") 19 | .foregroundStyle(.green) 20 | .font(.title2) 21 | 22 | VStack(alignment: .leading, spacing: 4) { 23 | Text("Đã cấp quyền thành công") 24 | .font(.headline) 25 | .foregroundStyle(.primary) 26 | 27 | Text("PHTV đã sẵn sàng hoạt động") 28 | .font(.subheadline) 29 | .foregroundStyle(.secondary) 30 | } 31 | 32 | Spacer() 33 | } 34 | .padding() 35 | .background { 36 | if #available(macOS 26.0, *) { 37 | RoundedRectangle(cornerRadius: 12) 38 | .fill(Color.green.opacity(0.1)) 39 | .glassEffect(in: .rect(cornerRadius: 12)) 40 | } else { 41 | Color.green.opacity(0.1) 42 | .cornerRadius(8) 43 | } 44 | } 45 | } else { 46 | // Warning state 47 | VStack(alignment: .leading, spacing: 12) { 48 | HStack(spacing: 12) { 49 | Image(systemName: "exclamationmark.triangle.fill") 50 | .foregroundStyle(.orange) 51 | .font(.title2) 52 | 53 | VStack(alignment: .leading, spacing: 4) { 54 | Text("Cần cấp quyền truy cập") 55 | .font(.headline) 56 | 57 | Text("PHTV cần quyền Accessibility để hoạt động") 58 | .font(.subheadline) 59 | .foregroundStyle(.secondary) 60 | } 61 | } 62 | 63 | if #available(macOS 26.0, *) { 64 | Button("Mở Cài đặt Hệ thống") { 65 | if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { 66 | NSWorkspace.shared.open(url) 67 | } 68 | } 69 | .buttonStyle(.glassProminent) 70 | .tint(.orange) 71 | } else { 72 | Button("Mở Cài đặt Hệ thống") { 73 | if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") { 74 | NSWorkspace.shared.open(url) 75 | } 76 | } 77 | .buttonStyle(.borderedProminent) 78 | } 79 | } 80 | .padding() 81 | .background { 82 | if #available(macOS 26.0, *) { 83 | RoundedRectangle(cornerRadius: 12) 84 | .fill(Color.orange.opacity(0.1)) 85 | .glassEffect(in: .rect(cornerRadius: 12)) 86 | } else { 87 | Color.orange.opacity(0.1) 88 | .cornerRadius(8) 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | #Preview("Chưa cấp quyền") { 96 | PermissionWarningView(hasPermission: false) 97 | .padding() 98 | } 99 | 100 | #Preview("Đã cấp quyền") { 101 | PermissionWarningView(hasPermission: true) 102 | .padding() 103 | } 104 | -------------------------------------------------------------------------------- /PHTV/Core/Platforms/mac.h: -------------------------------------------------------------------------------- 1 | // 2 | // mac.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | 10 | #ifndef PHTV_MAC_H 11 | #define PHTV_MAC_H 12 | 13 | //define Key code for mac keyboard 14 | #define KEY_ESC 53 15 | #define KEY_DELETE 51 16 | #define KEY_TAB 48 17 | #define KEY_ENTER 76 18 | #define KEY_RETURN 36 19 | #define KEY_SPACE 49 20 | #define KEY_LEFT 123 21 | #define KEY_RIGHT 124 22 | #define KEY_DOWN 125 23 | #define KEY_UP 126 24 | 25 | #define KEY_EMPTY 256 26 | #define KEY_A 0 27 | #define KEY_B 11 28 | #define KEY_C 8 29 | #define KEY_D 2 30 | #define KEY_E 14 31 | #define KEY_F 3 32 | #define KEY_G 5 33 | #define KEY_H 4 34 | #define KEY_I 34 35 | #define KEY_J 38 36 | #define KEY_K 40 37 | #define KEY_L 37 38 | #define KEY_M 46 39 | #define KEY_N 45 40 | #define KEY_O 31 41 | #define KEY_P 35 42 | #define KEY_Q 12 43 | #define KEY_R 15 44 | #define KEY_S 1 45 | #define KEY_T 17 46 | #define KEY_U 32 47 | #define KEY_V 9 48 | #define KEY_W 13 49 | #define KEY_X 7 50 | #define KEY_Y 16 51 | #define KEY_Z 6 52 | 53 | #define KEY_1 18 54 | #define KEY_2 19 55 | #define KEY_3 20 56 | #define KEY_4 21 57 | #define KEY_5 23 58 | #define KEY_6 22 59 | #define KEY_7 26 60 | #define KEY_8 28 61 | #define KEY_9 25 62 | #define KEY_0 29 63 | 64 | #define KEY_LEFT_BRACKET 33 65 | #define KEY_RIGHT_BRACKET 30 66 | 67 | #define KEY_LEFT_SHIFT 57 68 | #define KEY_RIGHT_SHIFT 60 69 | #define KEY_DOT 47 70 | 71 | #define KEY_BACKQUOTE 50 72 | #define KEY_MINUS 27 73 | #define KEY_EQUALS 24 74 | #define KEY_BACK_SLASH 42 75 | #define KEY_SEMICOLON 41 76 | #define KEY_QUOTE 39 77 | #define KEY_COMMA 43 78 | #define KEY_SLASH 44 79 | 80 | // Modifier keys (for restore key feature) 81 | #define KEY_LEFT_COMMAND 55 82 | #define KEY_RIGHT_COMMAND 54 83 | #define KEY_LEFT_CONTROL 59 84 | #define KEY_RIGHT_CONTROL 62 85 | #define KEY_LEFT_OPTION 58 86 | #define KEY_RIGHT_OPTION 61 87 | #define KEY_FUNCTION 63 88 | 89 | #endif //PHTV_MAC_H -------------------------------------------------------------------------------- /PHTV/SwiftUI/Utilities/PreviewHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewHelpers.swift 3 | // PHTV 4 | // 5 | // Preview helpers for SwiftUI development 6 | // Created by Phạm Hùng Tiến on 2026. 7 | // 8 | 9 | import SwiftUI 10 | 11 | // MARK: - Preview Data 12 | extension AppState { 13 | static var preview: AppState { 14 | let state = AppState.shared 15 | state.isEnabled = true 16 | state.checkSpelling = true 17 | state.useModernOrthography = true 18 | state.useMacro = true 19 | state.hasAccessibilityPermission = true 20 | return state 21 | } 22 | 23 | static var previewDisabled: AppState { 24 | let state = AppState.shared 25 | state.isEnabled = false 26 | state.hasAccessibilityPermission = false 27 | return state 28 | } 29 | } 30 | 31 | extension MacroItem { 32 | static var preview: [MacroItem] { 33 | [ 34 | MacroItem(shortcut: "addr", expansion: "123 Đường ABC, Quận XYZ, TP. HCM"), 35 | MacroItem(shortcut: "email", expansion: "example@email.com"), 36 | MacroItem(shortcut: "phone", expansion: "0123 456 789"), 37 | MacroItem(shortcut: "sig", expansion: "Trân trọng,\nPhạm Hùng Tiến"), 38 | MacroItem(shortcut: "ty", expansion: "Cảm ơn bạn"), 39 | ] 40 | } 41 | } 42 | 43 | // MARK: - Preview Wrappers 44 | struct PreviewWrapper: View { 45 | let content: Content 46 | 47 | init(@ViewBuilder content: () -> Content) { 48 | self.content = content() 49 | } 50 | 51 | var body: some View { 52 | content 53 | .environmentObject(AppState.preview) 54 | } 55 | } 56 | 57 | // MARK: - Previews for Main Views 58 | struct SettingsView_Previews: PreviewProvider { 59 | static var previews: some View { 60 | Group { 61 | // Light mode 62 | SettingsView() 63 | .environmentObject(AppState.preview) 64 | .previewDisplayName("Light Mode") 65 | 66 | // Dark mode 67 | SettingsView() 68 | .environmentObject(AppState.preview) 69 | .preferredColorScheme(.dark) 70 | .previewDisplayName("Dark Mode") 71 | } 72 | } 73 | } 74 | 75 | struct TypingSettingsView_Previews: PreviewProvider { 76 | static var previews: some View { 77 | Group { 78 | TypingSettingsView() 79 | .environmentObject(AppState.preview) 80 | .previewDisplayName("With Permission") 81 | 82 | TypingSettingsView() 83 | .environmentObject(AppState.previewDisabled) 84 | .previewDisplayName("No Permission") 85 | } 86 | } 87 | } 88 | 89 | struct MacroSettingsView_Previews: PreviewProvider { 90 | static var previews: some View { 91 | MacroSettingsView() 92 | .environmentObject(AppState.preview) 93 | } 94 | } 95 | 96 | struct SystemSettingsView_Previews: PreviewProvider { 97 | static var previews: some View { 98 | SystemSettingsView() 99 | .environmentObject(AppState.preview) 100 | } 101 | } 102 | 103 | struct AboutView_Previews: PreviewProvider { 104 | static var previews: some View { 105 | AboutView() 106 | } 107 | } 108 | 109 | // MARK: - Component Previews 110 | struct PermissionWarningView_Previews: PreviewProvider { 111 | static var previews: some View { 112 | Group { 113 | PermissionWarningView(hasPermission: false) 114 | .padding() 115 | .previewLayout(.sizeThatFits) 116 | 117 | PermissionWarningView(hasPermission: true) 118 | .padding() 119 | .preferredColorScheme(.dark) 120 | .previewLayout(.sizeThatFits) 121 | } 122 | } 123 | } 124 | 125 | struct HotkeyConfigView_Previews: PreviewProvider { 126 | static var previews: some View { 127 | HotkeyConfigView() 128 | .environmentObject(AppState.preview) 129 | .padding() 130 | .previewLayout(.sizeThatFits) 131 | } 132 | } 133 | 134 | struct MacroEditorView_Previews: PreviewProvider { 135 | static var previews: some View { 136 | MacroEditorView(isPresented: .constant(true)) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Views/Settings/ThemeSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemeSettingsView.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ThemeSettingsView: View { 12 | @EnvironmentObject var themeManager: ThemeManager 13 | 14 | var body: some View { 15 | ScrollView { 16 | VStack(spacing: 20) { 17 | // Predefined Colors 18 | SettingsCard(title: "Màu chủ đạo", icon: "paintpalette.fill") { 19 | VStack(spacing: 16) { 20 | // Color Grid 21 | LazyVGrid(columns: [ 22 | GridItem(.flexible()), 23 | GridItem(.flexible()), 24 | GridItem(.flexible()), 25 | GridItem(.flexible()), 26 | ], spacing: 12) { 27 | ForEach(themeManager.predefinedColors) { themeColor in 28 | ThemeColorButton( 29 | themeColor: themeColor, 30 | isSelected: isSameColor(themeColor.color, themeManager.themeColor) 31 | ) { 32 | withAnimation(.easeInOut(duration: 0.2)) { 33 | themeManager.themeColor = themeColor.color 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | Spacer(minLength: 20) 42 | } 43 | .padding(20) 44 | } 45 | .background(Color(NSColor.windowBackgroundColor)) 46 | } 47 | 48 | // Helper function to compare colors 49 | private func isSameColor(_ color1: Color, _ color2: Color) -> Bool { 50 | let nsColor1 = NSColor(color1) 51 | let nsColor2 = NSColor(color2) 52 | 53 | // Convert to RGB space for comparison 54 | guard let rgb1 = nsColor1.usingColorSpace(.deviceRGB), 55 | let rgb2 = nsColor2.usingColorSpace(.deviceRGB) else { 56 | return false 57 | } 58 | 59 | return abs(rgb1.redComponent - rgb2.redComponent) < 0.01 && 60 | abs(rgb1.greenComponent - rgb2.greenComponent) < 0.01 && 61 | abs(rgb1.blueComponent - rgb2.blueComponent) < 0.01 62 | } 63 | } 64 | 65 | // MARK: - Theme Color Button 66 | 67 | struct ThemeColorButton: View { 68 | let themeColor: ThemeColor 69 | let isSelected: Bool 70 | let action: () -> Void 71 | 72 | var body: some View { 73 | Button(action: action) { 74 | VStack(spacing: 8) { 75 | ZStack { 76 | Circle() 77 | .fill(themeColor.color) 78 | .frame(width: 44, height: 44) 79 | 80 | if isSelected { 81 | Circle() 82 | .strokeBorder(Color.primary, lineWidth: 3) 83 | .frame(width: 44, height: 44) 84 | 85 | Image(systemName: "checkmark") 86 | .font(.system(size: 16, weight: .bold)) 87 | .foregroundStyle(.white) 88 | } 89 | } 90 | 91 | Text(themeColor.name) 92 | .font(.caption) 93 | .foregroundStyle(isSelected ? .primary : .secondary) 94 | .fontWeight(isSelected ? .semibold : .regular) 95 | } 96 | .frame(maxWidth: .infinity) 97 | .padding(.vertical, 8) 98 | .background( 99 | RoundedRectangle(cornerRadius: 10) 100 | .fill(isSelected ? themeColor.color.opacity(0.1) : Color.clear) 101 | ) 102 | .overlay( 103 | RoundedRectangle(cornerRadius: 10) 104 | .stroke(isSelected ? themeColor.color.opacity(0.3) : Color.clear, lineWidth: 2) 105 | ) 106 | } 107 | .buttonStyle(.plain) 108 | } 109 | } 110 | 111 | #Preview { 112 | ThemeSettingsView() 113 | .frame(width: 600, height: 700) 114 | } 115 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Views/StatusBarMenuView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusBarMenuView.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct StatusBarMenuView: View { 12 | @EnvironmentObject var appState: AppState 13 | @Environment(\.openWindow) private var openWindow 14 | 15 | var body: some View { 16 | // Language toggle 17 | Button { 18 | appState.isEnabled = true 19 | } label: { 20 | HStack { 21 | Image(systemName: "v.circle.fill") 22 | Text("Tiếng Việt") 23 | if appState.isEnabled { 24 | Spacer() 25 | Image(systemName: "checkmark") 26 | } 27 | } 28 | } 29 | .keyboardShortcut("v", modifiers: [.control, .shift]) 30 | 31 | Button { 32 | appState.isEnabled = false 33 | } label: { 34 | HStack { 35 | Image(systemName: "e.circle.fill") 36 | Text("Tiếng Anh") 37 | if !appState.isEnabled { 38 | Spacer() 39 | Image(systemName: "checkmark") 40 | } 41 | } 42 | } 43 | 44 | Divider() 45 | 46 | // Input Method 47 | Menu { 48 | ForEach(InputMethod.allCases) { method in 49 | Button { 50 | appState.inputMethod = method 51 | } label: { 52 | HStack { 53 | Text(method.rawValue) 54 | if appState.inputMethod == method { 55 | Spacer() 56 | Image(systemName: "checkmark") 57 | } 58 | } 59 | } 60 | } 61 | } label: { 62 | Label("Kiểu gõ: \(appState.inputMethod.rawValue)", systemImage: "keyboard") 63 | } 64 | 65 | // Code Table 66 | Menu { 67 | ForEach(CodeTable.allCases) { table in 68 | Button { 69 | appState.codeTable = table 70 | } label: { 71 | HStack { 72 | Text(table.displayName) 73 | if appState.codeTable == table { 74 | Spacer() 75 | Image(systemName: "checkmark") 76 | } 77 | } 78 | } 79 | } 80 | } label: { 81 | Label("Bảng mã: \(appState.codeTable.displayName)", systemImage: "textformat") 82 | } 83 | 84 | Divider() 85 | 86 | // Quick toggles 87 | Toggle(isOn: $appState.checkSpelling) { 88 | Label("Kiểm tra chính tả", systemImage: "checkmark.seal") 89 | } 90 | 91 | Toggle(isOn: $appState.useMacro) { 92 | Label("Gõ tắt", systemImage: "text.badge.plus") 93 | } 94 | 95 | Divider() 96 | 97 | // Settings 98 | Button { 99 | openWindow(id: "settings") 100 | NSApp.activate(ignoringOtherApps: true) 101 | } label: { 102 | Label("Cài đặt...", systemImage: "gearshape") 103 | } 104 | .keyboardShortcut(",", modifiers: .command) 105 | 106 | Button { 107 | openWindow(id: "settings") 108 | NSApp.activate(ignoringOtherApps: true) 109 | // Switch to About tab after a short delay 110 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { 111 | NotificationCenter.default.post(name: NSNotification.Name("ShowAboutTab"), object: nil) 112 | } 113 | } label: { 114 | Label("Về PHTV...", systemImage: "info.circle") 115 | } 116 | 117 | Divider() 118 | 119 | Button { 120 | NSApplication.shared.terminate(nil) 121 | } label: { 122 | Label("Thoát PHTV", systemImage: "power") 123 | } 124 | .keyboardShortcut("q", modifiers: .command) 125 | } 126 | } 127 | 128 | #Preview { 129 | StatusBarMenuView() 130 | .environmentObject(AppState.shared) 131 | } 132 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🤝 Hướng dẫn Đóng góp 4 | 5 | **Cảm ơn bạn muốn đóng góp cho PHTV!** 6 | 7 | [🏠 Trang chủ](README.md) • [📋 Code of Conduct](CODE_OF_CONDUCT.md) • [🐛 Issues](../../issues) 8 | 9 |
10 | 11 | --- 12 | 13 | ## 📜 Quy tắc ứng xử 14 | 15 | Xem [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). Bằng cách tham gia, bạn đồng ý tuân thủ các quy tắc. 16 | 17 | ## 🚀 Bắt đầu nhanh 18 | 19 | 1. **Fork & Clone:** 20 | 21 | ```bash 22 | git clone https://github.com/YOUR_USERNAME/PHTV.git 23 | cd PHTV 24 | git remote add upstream https://github.com/PhamHungTien/PHTV.git 25 | ``` 26 | 27 | 2. **Tạo branch mới:** 28 | 29 | ```bash 30 | git checkout -b feature/your-name 31 | ``` 32 | 33 | 3. **Build & test:** 34 | 35 | ```bash 36 | open PHTV.xcodeproj 37 | ``` 38 | 39 | 4. **Commit & push:** 40 | 41 | ```bash 42 | git add . 43 | git commit -m "feat: Mô tả tính năng" 44 | git push origin feature/your-name 45 | ``` 46 | 47 | 5. **Tạo Pull Request** trên GitHub 48 | 49 | ## 🐛 Báo cáo lỗi 50 | 51 | Tạo [issue mới](../../issues/new) với thông tin: 52 | 53 | - Tiêu đề rõ ràng 54 | - Cách tái hiện (bước chi tiết) 55 | - Hành vi mong đợi vs thực tế 56 | - macOS version & PHTV version 57 | - Screenshot/video (nếu có) 58 | 59 | ## 💡 Đề xuất tính năng 60 | 61 | Tạo issue với nhãn `enhancement` bao gồm: 62 | 63 | - Vấn đề bạn cố gắng giải quyết 64 | - Giải pháp đề xuất 65 | - Giải pháp thay thế 66 | 67 | ## ✅ Pull Request 68 | 69 | - Rebase từ `upstream/main` trước khi push 70 | - Commit message: `feat:` hoặc `fix:` + mô tả 71 | - Liên kết issue nếu có 72 | - Thêm test nếu cần 73 | 74 | ## 📝 Commit Message 75 | 76 | Format: `: ` 77 | 78 | - `feat:` - Tính năng mới 79 | - `fix:` - Sửa lỗi 80 | - `docs:` - Cập nhật tài liệu 81 | - `style:` - Format code 82 | - `refactor:` - Tái cấu trúc 83 | - `test:` - Thêm test 84 | - `chore:` - Công việc khác 85 | 86 | ## 🔨 Hướng dẫn phát triển 87 | 88 | ### Cấu trúc dự án 89 | 90 | ``` 91 | PHTV/ 92 | ├── PHTV/ 93 | │ ├── Application/ # AppDelegate, main entry point 94 | │ ├── Core/ 95 | │ │ ├── Engine/ # Core input method engine (C++) 96 | │ │ │ ├── Engine.cpp/.h # Logic chính 97 | │ │ │ ├── Vietnamese.cpp/.h # Bảng mã tiếng Việt 98 | │ │ │ ├── Macro.cpp/.h # Quản lý macro 99 | │ │ │ └── ... 100 | │ │ └── Platforms/ # macOS-specific integration 101 | │ ├── Managers/ # Business logic 102 | │ ├── SwiftUI/ # Giao diện người dùng 103 | │ │ ├── Views/ # SwiftUI views 104 | │ │ ├── Controllers/ # Window/Status bar controllers 105 | │ │ └── Utilities/ # Helper functions 106 | │ └── Utils/ # Utility functions 107 | ├── PHTV.xcodeproj/ # Xcode project 108 | └── README.md 109 | ``` 110 | 111 | ### Build và Test 112 | 113 | ```bash 114 | # Build project 115 | xcodebuild -project PHTV.xcodeproj -scheme PHTV 116 | 117 | # Run tests (nếu có) 118 | xcodebuild -project PHTV.xcodeproj -scheme PHTV test 119 | 120 | # Clean build 121 | xcodebuild -project PHTV.xcodeproj clean 122 | ``` 123 | 124 | ### Debugging 125 | 126 | 1. **Trong Xcode:** 127 | 128 | - Nhấn Cmd+R để run 129 | - Sử dụng breakpoints (Cmd+\) 130 | - View console output (Cmd+Shift+C) 131 | 132 | 2. **Console logging:** 133 | ```swift 134 | print("Debug message: \(value)") 135 | ``` 136 | 137 | ## 📝 Quy tắc Code 138 | 139 | ### Swift Code Style 140 | 141 | - Sử dụng 4 spaces cho indentation 142 | - Tên biến và hàm: `camelCase` 143 | - Tên class và struct: `PascalCase` 144 | - Tên hằng số: `camelCase` hoặc `UPPER_CASE` 145 | - Viết comment cho các hàm public 146 | 147 | **Ví dụ:** 148 | 149 | ```swift 150 | /// Chuyển đổi giữa tiếng Việt và Anh 151 | /// - Parameter enabled: Bật/tắt tiếng Việt 152 | func toggleVietnameseMode(enabled: Bool) { 153 | // Logic ở đây 154 | } 155 | ``` 156 | 157 | ### Objective-C/C++ Code Style 158 | 159 | - Sử dụng 4 spaces cho indentation 160 | - PascalCase cho tên class/struct 161 | 162 | --- 163 | 164 |
165 | 166 | ## ✨ Cảm ơn đã đóng góp! 167 | 168 | Mọi đóng góp, dù lớn hay nhỏ, đều được trân trọng và ghi nhận. 169 | 170 | [![Contributors](https://img.shields.io/github/contributors/PhamHungTien/PHTV)](../../graphs/contributors) 171 | 172 | **[⬆️ Về đầu trang](#-hướng-dẫn-đóng-góp)** 173 | 174 | [🏠 Trang chủ](README.md) • [📦 Cài đặt](INSTALL.md) • [💬 FAQ](FAQ.md) 175 | 176 |
177 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # ❓ FAQ - Câu hỏi thường gặp 4 | 5 | **Giải đáp thắc mắc về PHTV** 6 | 7 | [🏠 Trang chủ](README.md) • [📦 Cài đặt](INSTALL.md) • [🤝 Đóng góp](CONTRIBUTING.md) 8 | 9 |
10 | 11 | --- 12 | 13 | ## 📥 Cài đặt & Cấu hình 14 | 15 | ### Q1: PHTV có tương thích với phiên bản macOS nào? 16 | 17 | **A:** PHTV hỗ trợ macOS 14.0+ (Sonoma trở lên). Hoạt động tốt trên Apple Silicon (M1/M2/M3) và Intel Macs. 18 | 19 | ### Q2: Làm sao để chuyển đổi giữa tiếng Anh và tiếng Việt? 20 | 21 | **A:** Nhấn phím tắt được cấu hình (mặc định `Ctrl + Shift`). Hoặc click vào Status Bar icon để chọn ngôn ngữ. 22 | 23 | ### Q3: Phương pháp gõ nào phù hợp nhất? 24 | 25 | **A:** 26 | 27 | - **Telex**: Phổ biến, dễ học (ơ=ow, ư=uw, â=aa, v.v.) 28 | - **VNI**: Gõ bằng số (1-9 cho các dấu) 29 | - **Simple Telex 1/2**: Biến thể đơn giản của Telex 30 | 31 | Hãy thử từng cái để tìm phù hợp nhất! 32 | 33 | ### Q4: Sử dụng font nào để xem tiếng Việt đúng nhất? 34 | 35 | **A:** PHTV hỗ trợ mọi font tiêu chuẩn: 36 | 37 | - **Unicode**: Mọi font hiện đại (khuyến khích) 38 | - **TCVN3**: Các font cũ hơn 39 | - **VNI Windows**: Nếu dùng các app cũ 40 | 41 | --- 42 | 43 | ## 🎯 Sử dụng 44 | 45 | ### Q5: Làm sao để tắt PHTV cho một ứng dụng cụ thể? 46 | 47 | **A:** 48 | 49 | 1. Mở Settings → Excluded Apps 50 | 2. Nhấn "+" và chọn ứng dụng 51 | 3. Khi sử dụng app đó, PHTV sẽ tự động tắt 52 | 53 | ### Q6: Macro (gõ tắt) hoạt động như thế nào? 54 | 55 | **A:** 56 | 57 | 1. Settings → Macros → "+" 58 | 2. Nhập từ viết tắt (VD: "tks") và nội dung (VD: "cảm ơn") 59 | 3. Khi gõ "tks" + Space, tự động thay thế bằng "cảm ơn" 60 | 61 | ### Q7: Có thể bỏ dấu khi gõ không? 62 | 63 | **A:** Có! Gõ bình thường mà không cần phím dấu. Ví dụ: 64 | 65 | - `ao` → `ào`, `áo`, `ảo`, v.v. (gõ thêm phím để thêm dấu) 66 | 67 | ### Q8: Làm sao để reset cài đặt về mặc định? 68 | 69 | **A:** 70 | 71 | ```bash 72 | defaults delete com.phtv.app 73 | ``` 74 | 75 | Hoặc trong Settings → Reset All (nếu có button này). 76 | 77 | --- 78 | 79 | ## Tính năng & Hiệu năng 80 | 81 | ### Q9: PHTV tiêu thụ bao nhiêu tài nguyên? 82 | 83 | **A:** Rất nhẹ! 84 | 85 | - **CPU**: < 1% khi không dùng 86 | - **Memory**: ~30-50 MB 87 | - **Disk**: ~50 MB 88 | 89 | ### Q10: Có thể tùy chỉnh phím tắt được không? 90 | 91 | **A:** Có! Settings → Keyboard Shortcuts 92 | 93 | - Thay đổi phím chuyển ngôn ngữ 94 | - Thay đổi phím gõ dấu (nếu cần) 95 | 96 | ### Q11: Ngoài tiếng Việt, có hỗ trợ ngôn ngữ khác không? 97 | 98 | **A:** Hiện tại chỉ hỗ trợ tiếng Việt. Tiếng Anh là ngôn ngữ mặc định của hệ thống. 99 | 100 | ### Q12: Spell checking hoạt động như thế nào? 101 | 102 | **A:** PHTV có từ điển tiếng Việt tích hợp: 103 | 104 | - Tự động kiểm tra chính tả 105 | - Gợi ý từ sai (khi bật tính năng này) 106 | - Hỗ trợ cả từ địa phương 107 | 108 | --- 109 | 110 | ## Bảo mật & Quyền riêng tư 111 | 112 | ### Q13: PHTV có gửi dữ liệu lên Internet không? 113 | 114 | **A:** Không! Hoàn toàn offline, không kết nối mạng, không thu thập dữ liệu. 115 | 116 | ### Q14: Dữ liệu được lưu ở đâu? 117 | 118 | **A:** Chỉ nằm trên máy của bạn: 119 | 120 | - Settings: `~/Library/Preferences/com.phtv.app.plist` 121 | - Macros: `~/Library/Application Support/PHTV/` 122 | 123 | ### Q15: Tại sao PHTV cần quyền Accessibility? 124 | 125 | **A:** Để giám sát phím gõ, chuyển ngôn ngữ, hoạt động trên mọi ứng dụng. Yêu cầu chuẩn của macOS. 126 | 127 | ## 🛠️ Khắc phục sự cố 128 | 129 | ### Q16: PHTV không hoạt động? 130 | 131 | **A:** 132 | 133 | 1. Kiểm tra quyền Accessibility 134 | 2. Tắt/bật lại PHTV 135 | 3. Restart ứng dụng gặp lỗi 136 | 4. Tạo issue trên GitHub 137 | 138 | ### Q17: Phím tắt không hoạt động? 139 | 140 | **A:** 141 | 142 | 1. Kiểm tra Settings → Keyboard Shortcuts 143 | 2. Kiểm tra System Preferences → Keyboard → Shortcuts 144 | 3. Tìm xung đột với ứng dụng khác 145 | 146 | ### Q18: Tiếng Việt gõ ra sai? 147 | 148 | **A:** Kiểm tra Input Method (Telex/VNI) và Character Set (Unicode/TCVN3). 149 | 150 | ## 🚀 Phát triển 151 | 152 | ### Q19: Làm sao để đóng góp? 153 | 154 | **A:** Xem [CONTRIBUTING.md](CONTRIBUTING.md) - Fork, tạo branch, commit, PR. 155 | 156 | ### Q20: Engine gõ là gì? 157 | 158 | **A:** Dựa trên [OpenKey](https://github.com/tuyenvm/OpenKey) - dự án mã nguồn mở tiếng Việt. 159 | 160 | --- 161 | 162 |
163 | 164 | ## 💬 Vẫn có câu hỏi? 165 | 166 | [![GitHub Discussions](https://img.shields.io/badge/GitHub-Discussions-green?logo=github)](../../discussions) 167 | [![Email](https://img.shields.io/badge/Email-hungtien10a7@gmail.com-blue?logo=gmail)](mailto:hungtien10a7@gmail.com) 168 | 169 | [🏠 Trang chủ](README.md) • [📦 Cài đặt](INSTALL.md) • [🐛 Báo lỗi](../../issues) 170 | 171 |
172 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Utilities/ViewModifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModifiers.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | // MARK: - Custom View Modifiers for consistent styling 12 | 13 | struct CardStyle: ViewModifier { 14 | func body(content: Content) -> some View { 15 | if #available(macOS 26.0, *) { 16 | content 17 | .padding() 18 | .glassEffect(in: .rect(cornerRadius: 12)) 19 | } else { 20 | content 21 | .padding() 22 | .background(Color(NSColor.controlBackgroundColor)) 23 | .cornerRadius(8) 24 | .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1) 25 | } 26 | } 27 | } 28 | 29 | struct SectionHeaderStyle: ViewModifier { 30 | func body(content: Content) -> some View { 31 | content 32 | .font(.headline) 33 | .foregroundColor(.primary) 34 | .padding(.top, 8) 35 | } 36 | } 37 | 38 | extension View { 39 | func cardStyle() -> some View { 40 | modifier(CardStyle()) 41 | } 42 | 43 | func sectionHeader() -> some View { 44 | modifier(SectionHeaderStyle()) 45 | } 46 | 47 | // Apply consistent defaults for TextField across the app 48 | @ViewBuilder 49 | func settingsTextField() -> some View { 50 | #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) || targetEnvironment(macCatalyst) 51 | if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *) { 52 | self 53 | .disableAutocorrection(true) 54 | .textInputAutocapitalization(.never) 55 | } else { 56 | self 57 | .disableAutocorrection(true) 58 | } 59 | #elseif os(macOS) 60 | if #available(macOS 12.0, *) { 61 | self 62 | .disableAutocorrection(true) 63 | } else { 64 | self 65 | } 66 | #else 67 | self 68 | #endif 69 | } 70 | 71 | // Rounded text area style for TextEditor and similar inputs 72 | func roundedTextArea() -> some View { 73 | self 74 | .padding(6) 75 | .background(Color(NSColor.controlBackgroundColor)) 76 | .cornerRadius(8) 77 | .overlay( 78 | RoundedRectangle(cornerRadius: 8) 79 | .stroke(Color.gray.opacity(0.3), lineWidth: 1) 80 | ) 81 | } 82 | } 83 | 84 | // MARK: - Custom Button Styles 85 | 86 | struct PrimaryButtonStyle: ButtonStyle { 87 | func makeBody(configuration: Configuration) -> some View { 88 | if #available(macOS 26.0, *) { 89 | configuration.label 90 | .padding(.horizontal, 16) 91 | .padding(.vertical, 8) 92 | .buttonStyle(.glassProminent) 93 | } else { 94 | configuration.label 95 | .padding(.horizontal, 16) 96 | .padding(.vertical, 8) 97 | .background(Color.accentColor) 98 | .foregroundColor(.white) 99 | .cornerRadius(6) 100 | .opacity(configuration.isPressed ? 0.8 : 1.0) 101 | } 102 | } 103 | } 104 | 105 | struct SecondaryButtonStyle: ButtonStyle { 106 | func makeBody(configuration: Configuration) -> some View { 107 | if #available(macOS 26.0, *) { 108 | configuration.label 109 | .padding(.horizontal, 16) 110 | .padding(.vertical, 8) 111 | .buttonStyle(.glass) 112 | } else { 113 | configuration.label 114 | .padding(.horizontal, 16) 115 | .padding(.vertical, 8) 116 | .background(Color(NSColor.controlBackgroundColor)) 117 | .foregroundColor(.primary) 118 | .cornerRadius(6) 119 | .overlay( 120 | RoundedRectangle(cornerRadius: 6) 121 | .stroke(Color.gray.opacity(0.3), lineWidth: 1) 122 | ) 123 | .opacity(configuration.isPressed ? 0.8 : 1.0) 124 | } 125 | } 126 | } 127 | 128 | // MARK: - Animations 129 | 130 | extension Animation { 131 | static let phtv = Animation.easeInOut(duration: 0.25) 132 | static let phtvSpring = Animation.spring(response: 0.3, dampingFraction: 0.7) 133 | } 134 | 135 | // MARK: - Color Extensions 136 | 137 | extension Color { 138 | static let phtvPrimary = Color.accentColor 139 | static let phtvSecondary = Color(NSColor.secondaryLabelColor) 140 | static let phtvBackground = Color(NSColor.windowBackgroundColor) 141 | static let phtvSurface = Color(NSColor.controlBackgroundColor) 142 | } 143 | -------------------------------------------------------------------------------- /PHTV/Core/PHTVConstants.h: -------------------------------------------------------------------------------- 1 | // 2 | // PHTVConstants.h 3 | // PHTV - Vietnamese Input Method 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | // Centralized Constants & Configuration 9 | // 10 | 11 | #ifndef PHTVConstants_h 12 | #define PHTVConstants_h 13 | 14 | #pragma mark - Application Info 15 | #define PHTV_APP_NAME "PHTV" 16 | #define PHTV_APP_FULL_NAME "PHTV - Bộ gõ Tiếng Việt" 17 | #define PHTV_AUTHOR "Phạm Hùng Tiến" 18 | #define PHTV_COPYRIGHT_YEAR "2026" 19 | #define PHTV_VERSION "1.0" 20 | #define PHTV_BUILD "1" 21 | 22 | #pragma mark - Engine Configuration 23 | #define PHTV_DEBUG_MODE 0 24 | #define PHTV_MAX_UNICODE_STRING 256 25 | #define PHTV_MAX_BUFFER_SIZE 1024 26 | 27 | #pragma mark - Input Methods 28 | typedef enum : int { 29 | PHTVInputMethodEnglish = 0, 30 | PHTVInputMethodVietnamese = 1 31 | } PHTVInputMethod; 32 | 33 | typedef enum : int { 34 | PHTVInputTypeTelex = 0, 35 | PHTVInputTypeVNI = 1, 36 | PHTVInputTypeSimpleTelex1 = 2, 37 | PHTVInputTypeSimpleTelex2 = 3 38 | } PHTVInputType; 39 | 40 | typedef enum : int { 41 | PHTVCodeTableUnicode = 0, 42 | PHTVCodeTableTCVN3 = 1, 43 | PHTVCodeTableVNIWindows = 2, 44 | PHTVCodeTableUnicodeComposite = 3, 45 | PHTVCodeTableCP1258 = 4 46 | } PHTVCodeTable; 47 | 48 | #pragma mark - Feature Flags 49 | typedef enum : int { 50 | PHTVFeatureDisabled = 0, 51 | PHTVFeatureEnabled = 1 52 | } PHTVFeatureState; 53 | 54 | #pragma mark - Processing States 55 | typedef enum : int { 56 | PHTVProcessUnknown = 0, 57 | PHTVProcessVowel, 58 | PHTVProcessTone, 59 | PHTVProcessMark, 60 | PHTVProcessConsonant, 61 | PHTVProcessEndConsonant, 62 | PHTVProcessModifyWord, 63 | PHTVProcessRestore, 64 | PHTVProcessMacro, 65 | PHTVProcessNewSession 66 | } PHTVProcessState; 67 | 68 | #pragma mark - Bit Masks (Optimized) 69 | // Character composition masks 70 | #define PHTV_MASK_CAPS 0x10000 71 | #define PHTV_MASK_TONE 0x20000 72 | #define PHTV_MASK_TONE_W 0x40000 73 | 74 | // Mark masks (diacritics) 75 | #define PHTV_MASK_MARK_ACUTE 0x80000 // Sắc (á) 76 | #define PHTV_MASK_MARK_GRAVE 0x100000 // Huyền (à) 77 | #define PHTV_MASK_MARK_HOOK 0x200000 // Hỏi (ả) 78 | #define PHTV_MASK_MARK_TILDE 0x400000 // Ngã (ã) 79 | #define PHTV_MASK_MARK_DOT 0x800000 // Nặng (ạ) 80 | #define PHTV_MASK_MARK_ALL 0xF80000 81 | 82 | // Character info masks 83 | #define PHTV_MASK_CHAR_CODE 0xFFFF 84 | #define PHTV_MASK_STANDALONE 0x1000000 85 | #define PHTV_MASK_IS_CHAR_CODE 0x2000000 86 | #define PHTV_MASK_PURE_CHAR 0x80000000 87 | 88 | // Special features 89 | #define PHTV_MASK_END_CONSONANT 0x4000 90 | #define PHTV_MASK_CONSONANT_ALLOWED 0x8000 91 | 92 | #pragma mark - Modifier Keys 93 | #define PHTV_GET_KEY(data) ((data) & 0xFF) 94 | #define PHTV_HAS_CONTROL(data) (((data) & 0x100) != 0) 95 | #define PHTV_HAS_OPTION(data) (((data) & 0x200) != 0) 96 | #define PHTV_HAS_COMMAND(data) (((data) & 0x400) != 0) 97 | #define PHTV_HAS_SHIFT(data) (((data) & 0x800) != 0) 98 | #define PHTV_HAS_BEEP(data) (((data) & 0x8000) != 0) 99 | 100 | #define PHTV_SET_KEY(data, key) ((data) = ((data) & ~0xFF) | (key)) 101 | #define PHTV_SET_CONTROL(data, val) ((data) |= ((val) << 8)) 102 | #define PHTV_SET_OPTION(data, val) ((data) |= ((val) << 9)) 103 | #define PHTV_SET_COMMAND(data, val) ((data) |= ((val) << 10)) 104 | 105 | #pragma mark - Character Utilities 106 | #define PHTV_LOW_BYTE(data) ((data) & 0xFF) 107 | #define PHTV_HIGH_BYTE(data) (((data) >> 8) & 0xFF) 108 | #define PHTV_GET_BOOL(data) ((data) ? 1 : 0) 109 | 110 | #pragma mark - Vowel Check 111 | #define PHTV_IS_VOWEL(code) \ 112 | ((code) == KEY_A || (code) == KEY_E || (code) == KEY_U || \ 113 | (code) == KEY_Y || (code) == KEY_I || (code) == KEY_O) 114 | 115 | #define PHTV_IS_CONSONANT(code) (!PHTV_IS_VOWEL(code)) 116 | 117 | #pragma mark - Performance Optimization 118 | // Inline functions for critical path 119 | static inline int phtv_is_valid_range(int val, int min, int max) { 120 | return (val >= min && val <= max); 121 | } 122 | 123 | static inline int phtv_clamp(int val, int min, int max) { 124 | return (val < min) ? min : ((val > max) ? max : val); 125 | } 126 | 127 | #endif /* PHTVConstants_h */ 128 | -------------------------------------------------------------------------------- /PHTV/Core/PHTVConfig.m: -------------------------------------------------------------------------------- 1 | // 2 | // PHTVConfig.m 3 | // PHTV - Vietnamese Input Method 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #import "PHTVConfig.h" 10 | 11 | // Configuration keys - centralized and type-safe 12 | static NSString * const kPHTVInputMethodKey = @"PHTV_InputMethod"; 13 | static NSString * const kPHTVInputTypeKey = @"PHTV_InputType"; 14 | static NSString * const kPHTVCodeTableKey = @"PHTV_CodeTable"; 15 | static NSString * const kPHTVFreeMarkKey = @"PHTV_FreeMark"; 16 | static NSString * const kPHTVModernOrthographyKey = @"PHTV_ModernOrthography"; 17 | static NSString * const kPHTVSpellCheckKey = @"PHTV_SpellCheck"; 18 | static NSString * const kPHTVQuickTelexKey = @"PHTV_QuickTelex"; 19 | static NSString * const kPHTVRestoreInvalidKey = @"PHTV_RestoreInvalid"; 20 | static NSString * const kPHTVSwitchKeyKey = @"PHTV_SwitchKey"; 21 | static NSString * const kPHTVGrayIconKey = @"PHTV_GrayIcon"; 22 | static NSString * const kPHTVShowDockKey = @"PHTV_ShowDock"; 23 | static NSString * const kPHTVRunOnStartupKey = @"PHTV_RunOnStartup"; 24 | 25 | @implementation PHTVConfig 26 | 27 | #pragma mark - Singleton 28 | 29 | + (instancetype)shared { 30 | static PHTVConfig *sharedInstance = nil; 31 | static dispatch_once_t onceToken; 32 | dispatch_once(&onceToken, ^{ 33 | sharedInstance = [[self alloc] init]; 34 | }); 35 | return sharedInstance; 36 | } 37 | 38 | - (instancetype)init { 39 | self = [super init]; 40 | if (self) { 41 | [self loadFromUserDefaults]; 42 | } 43 | return self; 44 | } 45 | 46 | #pragma mark - Load/Save 47 | 48 | - (void)loadFromUserDefaults { 49 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 50 | 51 | // Core settings 52 | self.inputMethod = (PHTVInputMethod)[defaults integerForKey:kPHTVInputMethodKey]; 53 | self.inputType = (PHTVInputType)[defaults integerForKey:kPHTVInputTypeKey]; 54 | self.codeTable = (PHTVCodeTable)[defaults integerForKey:kPHTVCodeTableKey]; 55 | 56 | // Features 57 | self.freeMarkEnabled = [defaults boolForKey:kPHTVFreeMarkKey]; 58 | self.modernOrthographyEnabled = [defaults boolForKey:kPHTVModernOrthographyKey]; 59 | self.spellCheckEnabled = [defaults boolForKey:kPHTVSpellCheckKey]; 60 | self.quickTelexEnabled = [defaults boolForKey:kPHTVQuickTelexKey]; 61 | self.restoreOnInvalidWordEnabled = [defaults boolForKey:kPHTVRestoreInvalidKey]; 62 | 63 | // UI 64 | self.grayIconEnabled = [defaults boolForKey:kPHTVGrayIconKey]; 65 | self.showIconOnDockEnabled = [defaults boolForKey:kPHTVShowDockKey]; 66 | self.runOnStartupEnabled = [defaults boolForKey:kPHTVRunOnStartupKey]; 67 | 68 | // Advanced 69 | self.switchKeyStatus = (int)[defaults integerForKey:kPHTVSwitchKeyKey]; 70 | if (self.switchKeyStatus == 0) { 71 | self.switchKeyStatus = 0x1FC; // Default: Command+Shift+V 72 | } 73 | } 74 | 75 | - (void)saveToUserDefaults { 76 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 77 | 78 | [defaults setInteger:self.inputMethod forKey:kPHTVInputMethodKey]; 79 | [defaults setInteger:self.inputType forKey:kPHTVInputTypeKey]; 80 | [defaults setInteger:self.codeTable forKey:kPHTVCodeTableKey]; 81 | [defaults setBool:self.freeMarkEnabled forKey:kPHTVFreeMarkKey]; 82 | [defaults setBool:self.modernOrthographyEnabled forKey:kPHTVModernOrthographyKey]; 83 | [defaults setBool:self.spellCheckEnabled forKey:kPHTVSpellCheckKey]; 84 | [defaults setBool:self.quickTelexEnabled forKey:kPHTVQuickTelexKey]; 85 | [defaults setBool:self.restoreOnInvalidWordEnabled forKey:kPHTVRestoreInvalidKey]; 86 | [defaults setBool:self.grayIconEnabled forKey:kPHTVGrayIconKey]; 87 | [defaults setBool:self.showIconOnDockEnabled forKey:kPHTVShowDockKey]; 88 | [defaults setBool:self.runOnStartupEnabled forKey:kPHTVRunOnStartupKey]; 89 | [defaults setInteger:self.switchKeyStatus forKey:kPHTVSwitchKeyKey]; 90 | 91 | [defaults synchronize]; 92 | } 93 | 94 | - (void)resetToDefaults { 95 | self.inputMethod = PHTVInputMethodVietnamese; 96 | self.inputType = PHTVInputTypeTelex; 97 | self.codeTable = PHTVCodeTableUnicode; 98 | self.freeMarkEnabled = NO; 99 | self.modernOrthographyEnabled = YES; 100 | self.spellCheckEnabled = YES; 101 | self.quickTelexEnabled = YES; 102 | self.restoreOnInvalidWordEnabled = YES; 103 | self.grayIconEnabled = NO; 104 | self.showIconOnDockEnabled = NO; 105 | self.runOnStartupEnabled = YES; 106 | self.switchKeyStatus = 0x1FC; 107 | 108 | [self saveToUserDefaults]; 109 | } 110 | 111 | - (NSDictionary *)exportSettings { 112 | return @{ 113 | @"inputMethod": @(self.inputMethod), 114 | @"inputType": @(self.inputType), 115 | @"codeTable": @(self.codeTable), 116 | @"features": @{ 117 | @"freeMark": @(self.freeMarkEnabled), 118 | @"modernOrthography": @(self.modernOrthographyEnabled), 119 | @"spellCheck": @(self.spellCheckEnabled), 120 | @"quickTelex": @(self.quickTelexEnabled) 121 | } 122 | }; 123 | } 124 | 125 | - (void)importSettings:(NSDictionary *)settings { 126 | if (settings[@"inputMethod"]) { 127 | self.inputMethod = [settings[@"inputMethod"] intValue]; 128 | } 129 | if (settings[@"inputType"]) { 130 | self.inputType = [settings[@"inputType"] intValue]; 131 | } 132 | // Import other settings... 133 | [self saveToUserDefaults]; 134 | } 135 | 136 | @end 137 | -------------------------------------------------------------------------------- /PHTV/Utils/PHTVUtilities.m: -------------------------------------------------------------------------------- 1 | // 2 | // PHTVUtilities.m 3 | // PHTV - Vietnamese Input Method 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #import "PHTVUtilities.h" 10 | 11 | @implementation PHTVUtilities 12 | 13 | #pragma mark - String Formatting 14 | 15 | + (NSString *)formatNumber:(NSNumber *)number { 16 | static NSNumberFormatter *formatter = nil; 17 | static dispatch_once_t onceToken; 18 | dispatch_once(&onceToken, ^{ 19 | formatter = [[NSNumberFormatter alloc] init]; 20 | formatter.numberStyle = NSNumberFormatterDecimalStyle; 21 | formatter.groupingSeparator = @"."; 22 | formatter.locale = [NSLocale localeWithLocaleIdentifier:@"vi_VN"]; 23 | }); 24 | return [formatter stringFromNumber:number]; 25 | } 26 | 27 | + (NSString *)buildDateString { 28 | NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; 29 | [formatter setDateFormat:@"dd/MM/yyyy"]; 30 | [formatter setLocale:[NSLocale localeWithLocaleIdentifier:@"vi_VN"]]; 31 | 32 | // Parse build date from __DATE__ macro 33 | NSString *buildDate = [NSString stringWithUTF8String:__DATE__]; 34 | NSDateFormatter *compiler = [[NSDateFormatter alloc] init]; 35 | [compiler setDateFormat:@"MMM dd yyyy"]; 36 | [compiler setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US"]]; 37 | 38 | NSDate *date = [compiler dateFromString:buildDate]; 39 | return date ? [formatter stringFromDate:date] : @"N/A"; 40 | } 41 | 42 | + (NSString *)versionString { 43 | NSString *version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; 44 | NSString *build = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; 45 | return [NSString stringWithFormat:@"%@ (build %@)", version, build]; 46 | } 47 | 48 | #pragma mark - Key Processing (Optimized with lookup tables) 49 | 50 | + (BOOL)isVowelKey:(int)keyCode { 51 | // Fast lookup using switch - compiler optimizes to jump table 52 | switch (keyCode) { 53 | case 0: // KEY_A 54 | case 14: // KEY_E 55 | case 34: // KEY_I 56 | case 31: // KEY_O 57 | case 32: // KEY_U 58 | case 16: // KEY_Y 59 | return YES; 60 | default: 61 | return NO; 62 | } 63 | } 64 | 65 | + (BOOL)isConsonantKey:(int)keyCode { 66 | return ![self isVowelKey:keyCode]; 67 | } 68 | 69 | + (BOOL)isMarkKey:(int)keyCode inputType:(PHTVInputType)type { 70 | if (type == PHTVInputTypeVNI) { 71 | return (keyCode >= 18 && keyCode <= 23); // 1-5 keys 72 | } else { 73 | // Telex: s, f, r, x, j 74 | return (keyCode == 1 || keyCode == 3 || keyCode == 15 || 75 | keyCode == 7 || keyCode == 38); 76 | } 77 | } 78 | 79 | + (BOOL)isToneKey:(int)keyCode inputType:(PHTVInputType)type { 80 | if (type == PHTVInputTypeVNI) { 81 | return (keyCode == 22); // KEY_6 82 | } else { 83 | return (keyCode == 13); // KEY_W 84 | } 85 | } 86 | 87 | #pragma mark - Character Conversion (Optimized) 88 | 89 | + (unichar)removeVietnameseTone:(unichar)character { 90 | // Fast tone removal using range checks 91 | static const unichar ranges[][3] = { 92 | {0xE0, 0xE5, 0x61}, // à-å -> a 93 | {0xE8, 0xEB, 0x65}, // è-ë -> e 94 | {0xEC, 0xEF, 0x69}, // ì-ï -> i 95 | {0xF2, 0xF6, 0x6F}, // ò-ö -> o 96 | {0xF9, 0xFC, 0x75}, // ù-ü -> u 97 | {0xC0, 0xC5, 0x41}, // À-Å -> A 98 | {0xC8, 0xCB, 0x45}, // È-Ë -> E 99 | {0xCC, 0xCF, 0x49}, // Ì-Ï -> I 100 | {0xD2, 0xD6, 0x4F}, // Ò-Ö -> O 101 | {0xD9, 0xDC, 0x55}, // Ù-Ü -> U 102 | }; 103 | 104 | for (int i = 0; i < 10; i++) { 105 | if (character >= ranges[i][0] && character <= ranges[i][1]) { 106 | return ranges[i][2]; 107 | } 108 | } 109 | return character; 110 | } 111 | 112 | + (unichar)addTone:(unichar)character toneType:(int)tone { 113 | // Optimized tone addition 114 | // Implementation depends on Vietnamese character table 115 | return character; // Placeholder 116 | } 117 | 118 | + (NSString *)normalizeVietnameseString:(NSString *)input { 119 | if (!input || input.length == 0) return @""; 120 | 121 | NSMutableString *result = [NSMutableString stringWithCapacity:input.length]; 122 | for (NSUInteger i = 0; i < input.length; i++) { 123 | unichar ch = [input characterAtIndex:i]; 124 | [result appendFormat:@"%C", [self removeVietnameseTone:ch]]; 125 | } 126 | return result; 127 | } 128 | 129 | #pragma mark - Threading (Optimized) 130 | 131 | + (void)executeOnMainThread:(dispatch_block_t)block { 132 | if ([NSThread isMainThread]) { 133 | block(); 134 | } else { 135 | dispatch_async(dispatch_get_main_queue(), block); 136 | } 137 | } 138 | 139 | + (void)executeOnBackgroundThread:(dispatch_block_t)block { 140 | dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); 141 | } 142 | 143 | + (void)executeAfterDelay:(NSTimeInterval)delay block:(dispatch_block_t)block { 144 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), 145 | dispatch_get_main_queue(), block); 146 | } 147 | 148 | #pragma mark - Cache 149 | 150 | static NSCache *_sharedCache = nil; 151 | 152 | + (void)clearCache { 153 | if (_sharedCache) { 154 | [_sharedCache removeAllObjects]; 155 | } 156 | } 157 | 158 | + (NSUInteger)cacheSize { 159 | return _sharedCache ? _sharedCache.countLimit : 0; 160 | } 161 | 162 | @end 163 | -------------------------------------------------------------------------------- /PHTV/Core/Engine/DataType.h: -------------------------------------------------------------------------------- 1 | // 2 | // DataType.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #ifndef DataType_h 10 | #define DataType_h 11 | 12 | #include 13 | 14 | using namespace std; 15 | 16 | //#define V_PLATFORM_MAC 1 17 | //#define V_PLATFORM_WINDOWS 2 18 | 19 | #define MAX_BUFF 32 20 | 21 | enum vKeyEvent { 22 | Keyboard, 23 | Mouse 24 | }; 25 | 26 | enum vKeyEventState { 27 | KeyDown, 28 | KeyUp, 29 | MouseDown, 30 | MouseUp 31 | }; 32 | 33 | enum vKeyInputType { 34 | vTelex = 0, 35 | vVNI, 36 | vSimpleTelex1, 37 | vSimpleTelex2 38 | }; 39 | 40 | typedef unsigned char Byte; 41 | typedef signed char Int8; 42 | typedef unsigned char Uint8; 43 | typedef unsigned short Uint16; 44 | typedef unsigned int Uint32; 45 | typedef unsigned long int Uint64; 46 | 47 | enum HoolCodeState { 48 | vDoNothing = 0, //do not do anything 49 | vWillProcess, //will reverse 50 | vBreakWord, //start new 51 | vRestore, //restore character to old char 52 | vReplaceMaro, //replace by macro 53 | vRestoreAndStartNewSession, //special flag: use for restore key if invalid word with break character (, . ") 54 | }; 55 | 56 | //bytes data for main program 57 | struct vKeyHookState { 58 | /* 59 | * 0: Do nothing 60 | * 1: Process 61 | * 2: Word break; 62 | * 3: Restore 63 | * 4: replace by macro 64 | */ 65 | Byte code; 66 | Byte backspaceCount; 67 | Byte newCharCount; 68 | 69 | /** 70 | * 1: Word Break 71 | * 2: Delete key 72 | * 3: Normal key 73 | * 4: Should not send empty character 74 | */ 75 | Byte extCode; 76 | 77 | Uint32 charData[MAX_BUFF]; //new character will be put in queue 78 | 79 | vector macroKey; //used for macro function; it is a key 80 | vector macroData; //used for macro function; it is keycode data 81 | }; 82 | 83 | #ifdef LINUX 84 | #include "../Platforms/linux.h" 85 | #elif _WIN32 86 | #include "../Platforms/win32.h" 87 | #else 88 | #include "../Platforms/mac.h" 89 | #endif 90 | 91 | //internal engine data 92 | #define CAPS_MASK 0x10000 93 | #define TONE_MASK 0x20000 94 | #define TONEW_MASK 0x40000 95 | 96 | /* 97 | * MARK MASK 98 | * 1: Dấu Sắc - á 99 | * 2: Dấu Huyền - à 100 | * 3: Dấu Hỏi - ả 101 | * 4: Dấu Ngã - ã 102 | * 5: dấu Nặng - ạ 103 | */ 104 | #define MARK1_MASK 0x80000 105 | #define MARK2_MASK 0x100000 106 | #define MARK3_MASK 0x200000 107 | #define MARK4_MASK 0x400000 108 | #define MARK5_MASK 0x800000 109 | 110 | //for checking has mark or not 111 | #define MARK_MASK 0xF80000 112 | 113 | //mark and get first 16 bytes character 114 | #define CHAR_MASK 0xFFFF 115 | 116 | //Check whether the data is create by standalone key or not (W) 117 | #define STANDALONE_MASK 0x1000000 118 | 119 | //Chec whether the data is keyboard code or character code 120 | #define CHAR_CODE_MASK 0x2000000 121 | 122 | #define PURE_CHARACTER_MASK 0x80000000 123 | 124 | //for special feature 125 | #define END_CONSONANT_MASK 0x4000 126 | #define CONSONANT_ALLOW_MASK 0x8000 127 | 128 | 129 | //Utilities macro 130 | #define IS_CONSONANT(keyCode) !(keyCode == KEY_A || keyCode == KEY_E || keyCode == KEY_U || keyCode == KEY_Y || keyCode == KEY_I || keyCode == KEY_O) 131 | //#define IS_MARK_KEY(keyCode) (keyCode == KEY_S || keyCode == KEY_F || keyCode == KEY_R || keyCode == KEY_J || keyCode == KEY_X) 132 | #define CHR(index) (Uint16)TypingWord[index] 133 | #define IS_SPECIALKEY(keyCode) \ 134 | (vInputType == vTelex ? \ 135 | keyCode == KEY_W || keyCode == KEY_E || keyCode == KEY_R || keyCode == KEY_O || keyCode == KEY_LEFT_BRACKET || \ 136 | keyCode == KEY_RIGHT_BRACKET || keyCode == KEY_A || keyCode == KEY_S || keyCode == KEY_D || keyCode == KEY_F || keyCode == KEY_J || \ 137 | keyCode == KEY_Z || keyCode == KEY_X || keyCode == KEY_W \ 138 | : (vInputType == vVNI ? \ 139 | keyCode == KEY_1 || keyCode == KEY_2 || keyCode == KEY_3 || keyCode == KEY_4 || \ 140 | keyCode == KEY_5 || keyCode == KEY_6 || keyCode == KEY_7 || keyCode == KEY_8 || keyCode == KEY_9 || keyCode == KEY_0 \ 141 | : (vInputType == vSimpleTelex1 ? \ 142 | keyCode == KEY_W || keyCode == KEY_E || keyCode == KEY_R || keyCode == KEY_O || keyCode == KEY_A || keyCode == KEY_S || \ 143 | keyCode == KEY_D || keyCode == KEY_F || keyCode == KEY_J || keyCode == KEY_Z || keyCode == KEY_X || keyCode == KEY_W \ 144 | : (vInputType == vSimpleTelex2 ? \ 145 | keyCode == KEY_W || keyCode == KEY_E || keyCode == KEY_R || keyCode == KEY_O || keyCode == KEY_A || keyCode == KEY_S || \ 146 | keyCode == KEY_D || keyCode == KEY_F || keyCode == KEY_J || keyCode == KEY_Z || keyCode == KEY_X || keyCode == KEY_W : false)))) 147 | 148 | //is VNI or Unicode compound... 149 | #define IS_DOUBLE_CODE(code) (code == 2 || code == 3) 150 | #define IS_VNI_CODE(code) (code == 2) 151 | #define IS_QUICK_TELEX_KEY(code) (_index > 0 && (code == KEY_C || code == KEY_G || code == KEY_K || code == KEY_N || code == KEY_Q || code == KEY_P || code == KEY_T) && \ 152 | (Uint16)TypingWord[_index-1] == code) 153 | 154 | #define IS_NUMBER_KEY(code) (code == KEY_1 || code == KEY_2 || code == KEY_3 || code == KEY_4 || code == KEY_5 || code == KEY_6 || code == KEY_7 || code == KEY_8 || code == KEY_9 || code == KEY_0) 155 | 156 | #endif /* DataType_h */ 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | PHTV Icon 4 | 5 | # PHTV 6 | 7 | ### Bộ gõ tiếng Việt hiện đại cho macOS 8 | 9 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 10 | [![macOS 14+](https://img.shields.io/badge/macOS-14+-blue.svg)](https://www.apple.com/macos/) 11 | [![Swift 6.0+](https://img.shields.io/badge/Swift-6.0+-orange.svg)](https://swift.org) 12 | [![GitHub release](https://img.shields.io/github/v/release/PhamHungTien/PHTV)](../../releases/latest) 13 | [![GitHub stars](https://img.shields.io/github/stars/PhamHungTien/PHTV)](../../stargazers) 14 | 15 | [**📥 Tải về**](https://phamhungtien.com/PHTV/) • [**📖 Tài liệu**](INSTALL.md) • [**🐛 Báo lỗi**](../../issues) • [**❓ FAQ**](FAQ.md) 16 | 17 |
18 | 19 | --- 20 | 21 | ## 🎯 Giới thiệu 22 | 23 | PHTV là bộ gõ tiếng Việt **offline, nhanh, và riêng tư** cho macOS 14+. Được phát triển bằng Swift/SwiftUI với engine C++ từ OpenKey, mang đến trải nghiệm gõ tiếng Việt mượt mà và tích hợp sâu vào hệ thống. 24 | 25 | ### ✨ Tính năng nổi bật 26 | 27 | - 🚀 **Hoàn toàn offline** - Không cần Internet, bảo mật tuyệt đối 28 | - ⌨️ **Telex & VNI** - Đầy đủ các phương pháp gõ phổ biến 29 | - 🎨 **Native macOS** - Giao diện SwiftUI, hỗ trợ Dark Mode 30 | - 🔍 **Spotlight Fix** - Gõ tiếng Việt trong Spotlight không bị lỗi 31 | - 📝 **Macro** - Gõ tắt thông minh, import từ file 32 | - 🎛️ **Hot Reload** - Thay đổi cài đặt không cần khởi động lại 33 | 34 | ## 📸 Screenshots 35 | 36 |
37 | 38 | ### 🎨 Menu Bar 39 | 40 | 41 | 42 | 46 | 50 | 51 |
43 | Các kiểu gõ trên menu bar 44 |

Các kiểu gõ trên menu bar

45 |
47 | Các bảng mã trên menu bar 48 |

Các bảng mã trên menu bar

49 |
52 | 53 | ### ⚙️ Settings 54 | 55 | 56 | 57 | 61 | 65 | 69 | 70 |
58 | Settings - Typing 59 |

Typing Settings

60 |
62 | Settings - Macros 63 |

Macros Settings

64 |
66 | Settings - System 67 |

System Settings

68 |
71 | 72 |
73 | 74 | ## ⚡ Cài đặt nhanh 75 | 76 | **Phương pháp 1: Tải trực tiếp** (khuyên dùng) 77 | 78 | ```bash 79 | # Tải từ website 80 | open https://phamhungtien.com/PHTV/ 81 | 82 | # Hoặc từ GitHub Releases 83 | open https://github.com/PhamHungTien/PHTV/releases/latest 84 | ``` 85 | 86 | **Phương pháp 2: Build từ source** 87 | 88 | ```bash 89 | git clone https://github.com/PhamHungTien/PHTV.git 90 | cd PHTV 91 | open PHTV.xcodeproj 92 | # Build với Cmd+B, chạy với Cmd+R 93 | ``` 94 | 95 | > ⚠️ **Lưu ý**: Ứng dụng cần quyền **Accessibility** để hoạt động. Vào **System Settings > Privacy & Security > Accessibility** và thêm PHTV. 96 | 97 | ## 📚 Sử dụng 98 | 99 | ### Phím tắt 100 | 101 | | Phím tắt | Chức năng | 102 | | ------------------- | -------------------------------- | 103 | | **Control + Shift** | Chuyển Việt/Anh (tùy chỉnh được) | 104 | | **Fn + Modifier** | Phím tắt nâng cao (v1.1.2+) | 105 | 106 | ### Menu Bar 107 | 108 | Click biểu tượng **Vi** (Việt) / **En** (Anh) trên menu bar: 109 | 110 | - Chuyển đổi phương pháp gõ (Telex/VNI/Simple Telex) 111 | - Thay đổi bảng mã (Unicode/TCVN3/VNI Windows) 112 | - Bật/tắt kiểm tra chính tả, gõ tắt 113 | - Mở Settings để cấu hình chi tiết 114 | 115 | ### Settings 116 | 117 | - **Typing**: Phương pháp gõ, bảng mã, chính tả hiện đại 118 | - **Macros**: Quản lý gõ tắt, import/export từ file 119 | - **Excluded Apps**: Danh sách app tự động chuyển sang Anh 120 | - **System**: Khởi động cùng macOS, hotkey tùy chỉnh 121 | 122 | ## 🔧 Yêu cầu hệ thống 123 | 124 | | Thành phần | Yêu cầu | 125 | | ------------- | ----------------------------------------- | 126 | | **macOS** | 14.0+ (Sonoma trở lên) | 127 | | **Kiến trúc** | Apple Silicon (arm64) hoặc Intel (x86_64) | 128 | | **Xcode** | 16.0+ (nếu build từ source) | 129 | | **Quyền** | Accessibility | 130 | 131 | ## 🛠️ Công nghệ 132 | 133 | - **Swift 6.0** + **SwiftUI** - Giao diện native hiện đại 134 | - **C++** - Engine xử lý input (từ OpenKey) 135 | - **CGEvent API** - Event interception và xử lý bàn phím 136 | - **NSUserDefaults** - Lưu trữ cấu hình local 137 | 138 | ## 🤝 Đóng góp 139 | 140 | Mọi đóng góp đều được chào đón! Xem [CONTRIBUTING.md](CONTRIBUTING.md) để biết cách thức. 141 | 142 | **Các cách đóng góp:** 143 | 144 | - 🐛 [Báo lỗi](../../issues/new?template=bug_report.md) 145 | - 💡 [Đề xuất tính năng](../../issues/new?template=feature_request.md) 146 | - 🔧 Gửi Pull Request 147 | - 📝 Cải thiện tài liệu 148 | 149 | ## 📞 Hỗ trợ & Liên hệ 150 | 151 | - 📧 Email: hungtien10a7@gmail.com 152 | - 🐙 GitHub: [Issues](../../issues) • [Discussions](../../discussions) 153 | - 🌐 Website: [phamhungtien.com/PHTV](https://phamhungtien.com/PHTV/) 154 | - 👤 Facebook: [phamhungtien1404](https://www.facebook.com/phamhungtien1404) 155 | - 💼 LinkedIn: [Phạm Hùng Tiến](https://www.linkedin.com/in/ph%E1%BA%A1m-h%C3%B9ng-ti%E1%BA%BFn-a1b405327/) 156 | 157 | ## 📄 License & Credits 158 | 159 | PHTV được phát hành dưới giấy phép **[GPL v3.0](LICENSE)**. 160 | 161 | Dự án kế thừa và mở rộng engine từ **[OpenKey](https://github.com/tuyenvm/OpenKey)** của Tuyến Võ Minh. Chân thành cảm ơn cộng đồng OpenKey đã tạo nền tảng tuyệt vời này. 162 | 163 | --- 164 | 165 |
166 | 167 | ### ⭐ Nếu PHTV hữu ích, hãy cho dự án một star! 168 | 169 | [![GitHub stars](https://img.shields.io/github/stars/PhamHungTien/PHTV?style=social)](../../stargazers) 170 | 171 | **[⬆️ Về đầu trang](#phtv)** 172 | 173 | Made with ❤️ for Vietnamese macOS users 174 | 175 |
176 | -------------------------------------------------------------------------------- /PHTV.icon/Assets/1289679474.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 42 | 48 | 52 | 71 | 80 | 86 | 92 | 98 | 104 | 110 | 112 | 114 | 116 | 118 | 120 | image/svg+xml 123 | 126 | 129 | 131 | 134 | Openclipart 137 | 139 | 141 | Star 144 | 2010-11-13T20:17:55 147 | A star with simple shadowing 150 | https://openclipart.org/detail/95431/star-by-artokem 153 | 155 | 157 | artokem 160 | 162 | 164 | 166 | 168 | star 171 | 173 | 175 | 177 | 180 | 183 | 186 | 189 | 191 | 193 | 195 | 197 | -------------------------------------------------------------------------------- /PHTV/Core/Engine/ConvertTool.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // ConvertTool.cpp 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | #include 9 | #include 10 | #include "ConvertTool.h" 11 | #include "Engine.h" 12 | #include 13 | #include 14 | 15 | //option 16 | bool convertToolDontAlertWhenCompleted = false; 17 | bool convertToolToAllCaps = false; 18 | bool convertToolToAllNonCaps = false; 19 | bool convertToolToCapsFirstLetter = false; 20 | bool convertToolToCapsEachWord = false; 21 | bool convertToolRemoveMark = false; 22 | Uint8 convertToolFromCode = 0; 23 | Uint8 convertToolToCode = 0; 24 | int convertToolHotKey = 0; 25 | 26 | static vector _breakCode = {'.', '?', '!'}; 27 | 28 | static bool findKeyCode(const Uint32& charCode, const Uint8& code, int& j, int& k) { 29 | //find character which has tone/mark 30 | for (map>::iterator it = _codeTable[code].begin(); it != _codeTable[code].end(); ++it) { 31 | for (int z = 0; z < it->second.size(); z++) { 32 | if (charCode == it->second[z]) { 33 | j = it->first; 34 | k = z; 35 | return true; 36 | }//end if 37 | } 38 | } 39 | return false; 40 | } 41 | 42 | static Uint16 getUnicodeCompoundMarkIndex(const Uint16& mark) { 43 | for (int i = 0; i < 5; i++) { 44 | if (mark == _unicodeCompoundMark[i]) { 45 | return ((i + 1) << 13); 46 | } 47 | } 48 | return 0; 49 | } 50 | 51 | string convertUtil(const string& sourceString) { 52 | wstring data = utf8ToWideString(sourceString); 53 | Uint16 t = 0, target; 54 | int j, k, p; 55 | vector _temp; 56 | bool hasBreak = false; 57 | bool shouldUpperCase = false; 58 | if (convertToolToCapsFirstLetter || convertToolToCapsEachWord) 59 | shouldUpperCase = true; 60 | if (convertToolToAllNonCaps) 61 | shouldUpperCase = false; 62 | 63 | for (int i = 0; i < data.size(); i++) { 64 | p = 0; 65 | //find char with tone/mark 66 | if (i < data.size() - 1) { 67 | switch (convertToolFromCode) { 68 | case 2: //VNI 69 | case 4: //1258 70 | t = (Uint16)data[i] | (data[i+1] << 8); 71 | p = 1; 72 | break; 73 | case 3:{ //Unicode Compound 74 | target = getUnicodeCompoundMarkIndex(data[i+1]); 75 | if (target > 0){ 76 | t = (Uint16)data[i] | target; 77 | p = 1; 78 | } else { 79 | t = (Uint16)data[i]; 80 | } 81 | break; 82 | } 83 | default: 84 | t = (Uint16)data[i]; 85 | break; 86 | } 87 | 88 | if (findKeyCode(t, convertToolFromCode, j, k)) { 89 | i += p; 90 | target = _codeTable[convertToolToCode][j][k]; 91 | if ((convertToolToAllCaps || shouldUpperCase) && k % 2 != 0) { 92 | target = _codeTable[convertToolToCode][j][k-1]; 93 | } else if ((convertToolToAllNonCaps || !shouldUpperCase) && k % 2 == 0) { 94 | target = _codeTable[convertToolToCode][j][k+1]; 95 | } 96 | 97 | //remove mark/tone 98 | if (convertToolRemoveMark) { 99 | target = keyCodeToCharacter((Uint8)j); 100 | if (convertToolToAllCaps) { 101 | target = towupper(target); 102 | } else if (convertToolToAllNonCaps) { 103 | target = towlower(target); 104 | } 105 | } 106 | 107 | if (convertToolToCode == 0 || convertToolToCode == 1) { //Unicode 108 | _temp.push_back(target); 109 | } else if (convertToolToCode == 2 || convertToolToCode == 4) { //VNI, VN Locale 1258 110 | if (HIBYTE(target) > 32) { 111 | _temp.push_back((Uint8)target); 112 | _temp.push_back(target>>8); 113 | } else { 114 | _temp.push_back((Uint8)target); 115 | } 116 | } else if (convertToolToCode == 3) { //Unicode Compound 117 | if ((target >> 13) > 0) { 118 | _temp.push_back(target & 0x1FFF); 119 | _temp.push_back(_unicodeCompoundMark[(target>>13) - 1]); 120 | } else { 121 | _temp.push_back(target); 122 | } 123 | } 124 | shouldUpperCase = false; 125 | hasBreak = false; 126 | continue; 127 | } 128 | } 129 | 130 | //find primary keycode first 131 | t = (Uint16)data[i]; 132 | if (findKeyCode(t, convertToolFromCode, j, k)) { 133 | target = _codeTable[convertToolToCode][j][k]; 134 | if ((convertToolToAllCaps || shouldUpperCase) && k % 2 != 0) { 135 | target = _codeTable[convertToolToCode][j][k-1]; 136 | } else if ((convertToolToAllNonCaps || !shouldUpperCase) && k % 2 == 0) { 137 | target = _codeTable[convertToolToCode][j][k+1]; 138 | } 139 | 140 | //remove mark/tone 141 | if (convertToolRemoveMark) { 142 | target = keyCodeToCharacter((Uint8)j); 143 | if (convertToolToAllCaps) { 144 | target = towupper(target); 145 | } else if (convertToolToAllNonCaps){ 146 | target = towlower(target); 147 | } 148 | } 149 | 150 | _temp.push_back(target); 151 | shouldUpperCase = false; 152 | hasBreak = false; 153 | continue; 154 | } 155 | 156 | //if dont find => normal char 157 | if (convertToolToAllCaps || shouldUpperCase) 158 | _temp.push_back(towupper(data[i])); 159 | else if (convertToolToAllNonCaps || !shouldUpperCase) 160 | _temp.push_back(towlower(data[i])); 161 | else 162 | _temp.push_back(data[i]); 163 | 164 | if (t == '\n' || (hasBreak && t == ' ')) { 165 | if (convertToolToCapsFirstLetter || convertToolToCapsEachWord) 166 | shouldUpperCase = true; 167 | } else if (t == ' ' && convertToolToCapsEachWord) { 168 | shouldUpperCase = true; 169 | } else if (std::find(_breakCode.begin(), _breakCode.end(), t) != _breakCode.end()) { 170 | hasBreak = true; 171 | } else { 172 | shouldUpperCase = false; 173 | hasBreak = false; 174 | } 175 | } 176 | _temp.push_back(0); 177 | wstring str(_temp.begin(), _temp.end()); 178 | return wideStringToUtf8(str); 179 | } 180 | 181 | -------------------------------------------------------------------------------- /PHTV/Core/Engine/Engine.h: -------------------------------------------------------------------------------- 1 | // 2 | // Engine.h 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | #ifndef Engine_h 10 | #define Engine_h 11 | 12 | #include 13 | #include 14 | 15 | #include "DataType.h" 16 | #include "Vietnamese.h" 17 | #include "Macro.h" 18 | #include "SmartSwitchKey.h" 19 | #include "ConvertTool.h" 20 | 21 | #define IS_DEBUG 1 22 | 23 | #ifndef LOBYTE 24 | #define LOBYTE(data) (data & 0xFF) 25 | #endif // !LOBYTE 26 | #ifndef HIBYTE 27 | #define HIBYTE(data) ((data>>8) & 0xFF) 28 | #endif // !HIBYTE 29 | 30 | #define GET_SWITCH_KEY(data) (data & 0xFF) 31 | #define HAS_CONTROL(data) ((data & 0x100) ? 1 : 0) 32 | #define HAS_OPTION(data) ((data & 0x200) ? 1 : 0) 33 | #define HAS_COMMAND(data) ((data & 0x400) ? 1 : 0) 34 | #define HAS_SHIFT(data) ((data & 0x800) ? 1 : 0) 35 | #define HAS_FN(data) ((data & 0x1000) ? 1 : 0) 36 | #define GET_BOOL(data) (data ? 1 : 0) 37 | #define HAS_BEEP(data) (data & 0x8000) 38 | #define SET_SWITCH_KEY(data, key) data = (data & 0xFF) | key 39 | #define SET_CONTROL_KEY(data, val) data|=val<<8; 40 | #define SET_OPTION_KEY(data, val) data|=val<<9; 41 | #define SET_COMMAND_KEY(data, val) data|=val<<10; 42 | 43 | //define these variable in your application 44 | //API 45 | /* 46 | * 0: English 47 | * 1: Vietnamese 48 | * VOLATILE: Read by event tap thread, written by main thread 49 | */ 50 | extern volatile int vLanguage; 51 | 52 | /* 53 | * 0: Telex 54 | * 1: VNI 55 | * VOLATILE: Read by event tap thread, written by main thread 56 | */ 57 | extern volatile int vInputType; 58 | 59 | /** 60 | * 0: No 61 | * 1: Yes 62 | */ 63 | extern int vFreeMark; 64 | 65 | /* 66 | * 0: Unicode 67 | * 1: TCVN3 (ABC) 68 | * 2: VNI-Windows 69 | * VOLATILE: Read by event tap thread, written by main thread 70 | */ 71 | extern volatile int vCodeTable; 72 | 73 | /* 74 | * first 8 bit: keycode 75 | * bit 8: Control on/off 76 | * bit 9: Option on/off 77 | * bit 10: Command on/off 78 | * bit 11: Shift on/off 79 | * bit 12: Fn on/off 80 | * bit 15: Beep on/off 81 | */ 82 | extern volatile int vSwitchKeyStatus; 83 | 84 | /** 85 | * 0: No 86 | * 1: Yes 87 | */ 88 | extern volatile int vCheckSpelling; 89 | 90 | /* 91 | * 0: òa, úy 92 | * 1: oà uý 93 | */ 94 | extern volatile int vUseModernOrthography; 95 | 96 | /** 97 | * 0: No 98 | * 1: Yes 99 | * (cc=ch, gg=gi, kk=kh, nn=ng, qq=qu, pp=ph, tt=th, uu=ươ) 100 | */ 101 | extern volatile int vQuickTelex; 102 | 103 | /** 104 | * Work together with vCheckSpelling 105 | * 0: No 106 | * 1: Yes 107 | * 108 | */ 109 | extern volatile int vRestoreIfWrongSpelling; 110 | 111 | /* 112 | * Fix recommend browser's address, excel,... 113 | * 0: No 114 | * 1: Yes 115 | */ 116 | extern volatile int vFixRecommendBrowser; 117 | 118 | /** 119 | * Macro on or off 120 | */ 121 | extern volatile int vUseMacro; 122 | 123 | /** 124 | * Still use macro if you are in english mode 125 | */ 126 | extern volatile int vUseMacroInEnglishMode; 127 | 128 | /** 129 | * Ex: define: btw -> by the way 130 | * Type: `Btw` -> `By the way` 131 | * Type: `BTW` -> `BY THE WAY` 132 | */ 133 | extern volatile int vAutoCapsMacro; 134 | 135 | /** 136 | * auto switch language when switch app 137 | * 0: No 138 | * 1: Yes 139 | */ 140 | extern volatile int vUseSmartSwitchKey; 141 | 142 | /** 143 | * Auto write upper case character for first letter. 144 | * 0: No 145 | * 1: Yes 146 | */ 147 | extern volatile int vUpperCaseFirstChar; 148 | 149 | /** 150 | * Temporarily turn off spell checking with Ctrl key 151 | * 0: No 152 | * 1: Yes 153 | */ 154 | extern volatile int vTempOffSpelling; 155 | 156 | /** 157 | * Allow write word with consonant Z, F, W, J 158 | * 0: No 159 | * 1: Yes 160 | */ 161 | extern volatile int vAllowConsonantZFWJ; 162 | 163 | /** 164 | * 0: No; 1: Yes 165 | * f -> ph: fanh -> phanh,... 166 | * j ->gi: jang -> giang,... 167 | * w ->qu: wen -> quen,... 168 | */ 169 | extern volatile int vQuickStartConsonant; 170 | 171 | /** 172 | * 0: No; 1: Yes 173 | * g -> ng: hag -> hang,... 174 | * h -> nh: vih -> vinh,... 175 | * k -> ch: bak -> bach,... 176 | */ 177 | extern volatile int vQuickEndConsonant; 178 | 179 | /** 180 | * 0: No; 1: Yes 181 | * Auto remember table code for each application 182 | */ 183 | extern volatile int vRememberCode; 184 | 185 | /** 186 | * 0: No; 1: Yes 187 | * Turn off Vietnamese when typing in another language. 188 | */ 189 | extern volatile int vOtherLanguage; 190 | 191 | /** 192 | * 0: No; 1: Yes 193 | * Temporarily turn off PHTV by hot key (Command on mac, Alt on Windows and Linux) 194 | */ 195 | extern volatile int vTempOffPHTV; 196 | 197 | /** 198 | * Restore to raw keys feature (default: ON) 199 | * When enabled, pressing ESC (or custom key) will restore typed text to raw keys 200 | * (user → úẻ → restore key → user) 201 | */ 202 | extern volatile int vRestoreOnEscape; 203 | 204 | /** 205 | * Custom key code for restore function (default: KEY_ESC = 53) 206 | * Can be any key: ESC, Option, Control 207 | * Set to 0 to use default ESC key 208 | */ 209 | extern volatile int vCustomEscapeKey; 210 | 211 | /** 212 | * Enable pause Vietnamese input when holding a key 213 | * When enabled, holding the pause key temporarily switches to English mode 214 | */ 215 | extern volatile int vPauseKeyEnabled; 216 | 217 | /** 218 | * Custom key code for pause function (default: KEY_LEFT_OPTION = 58) 219 | * Can be any key: Option, Control, Shift, Command, Fn, or any other key 220 | */ 221 | extern volatile int vPauseKey; 222 | 223 | /** 224 | * Call this function first to receive data pointer 225 | */ 226 | void* vKeyInit(); 227 | 228 | /** 229 | * Convert engine character to real character 230 | */ 231 | Uint32 getCharacterCode(const Uint32& data); 232 | 233 | /** 234 | * MAIN entry point for each key 235 | * event: mouse or keyboard event 236 | * state: additional state for event 237 | * data: key code 238 | * isCaps: caplock is on or shift key is pressing 239 | * otherControlKey: ctrl, option,... is pressing 240 | */ 241 | void vKeyHandleEvent(const vKeyEvent& event, 242 | const vKeyEventState& state, 243 | const Uint16& data, 244 | const Uint8& capsStatus=0, 245 | const bool& otherControlKey=false); 246 | 247 | /** 248 | * Start a new word 249 | */ 250 | void startNewSession(); 251 | 252 | /** 253 | * do some task in english mode (use for macro) 254 | */ 255 | void vEnglishMode(const vKeyEventState& state, const Uint16& data, const bool& isCaps, const bool& otherControlKey); 256 | 257 | /** 258 | * temporarily turn off spell checking 259 | */ 260 | void vTempOffSpellChecking(); 261 | 262 | /** 263 | * reset spelling value 264 | */ 265 | void vSetCheckSpelling(); 266 | 267 | /** 268 | * temporarily turn off PHTV engine 269 | */ 270 | void vTempOffEngine(const bool& off=true); 271 | 272 | /** 273 | * Manually trigger restore to raw keys 274 | * Returns true if restore was successful 275 | */ 276 | bool vRestoreToRawKeys(); 277 | 278 | /** 279 | * some utils function 280 | */ 281 | wstring utf8ToWideString(const string& str); 282 | string wideStringToUtf8(const wstring& str); 283 | 284 | #endif /* Engine_h */ 285 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Views/Components/MacroEditorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MacroEditorView.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct MacroEditorView: View { 12 | @Binding var isPresented: Bool 13 | @EnvironmentObject var appState: AppState 14 | @State private var macroName = "" 15 | @State private var macroCode = "" 16 | @State private var errorMessage = "" 17 | @State private var showError = false 18 | 19 | // Edit mode support 20 | var editingMacro: MacroItem? = nil 21 | var isEditMode: Bool { editingMacro != nil } 22 | 23 | var body: some View { 24 | VStack(spacing: 16) { 25 | HStack { 26 | Text(isEditMode ? "Chỉnh sửa gõ tắt" : "Thêm gõ tắt mới") 27 | .font(.headline) 28 | Spacer() 29 | Button("Đóng") { 30 | isPresented = false 31 | } 32 | } 33 | 34 | Form { 35 | Section("Tên gõ tắt") { 36 | TextField("Ví dụ: tvn (Việt Nam)", text: $macroName) 37 | .settingsTextField() 38 | .textFieldStyle(.roundedBorder) 39 | } 40 | 41 | Section("Nội dung") { 42 | TextEditor(text: $macroCode) 43 | .frame(minHeight: 100) 44 | .font(.system(.body, design: .monospaced)) 45 | .roundedTextArea() 46 | } 47 | } 48 | 49 | HStack { 50 | Spacer() 51 | Button("Hủy", role: .cancel) { 52 | isPresented = false 53 | } 54 | Button("Lưu") { 55 | saveMacro() 56 | } 57 | .buttonStyle(.borderedProminent) 58 | .disabled(macroName.isEmpty || macroCode.isEmpty) 59 | } 60 | .padding() 61 | } 62 | .padding() 63 | .frame(minWidth: 400, minHeight: 300) 64 | .alert("Lỗi", isPresented: $showError) { 65 | Button("OK") {} 66 | } message: { 67 | Text(errorMessage) 68 | } 69 | .onAppear { 70 | // Initialize fields if editing 71 | if let macro = editingMacro { 72 | macroName = macro.shortcut 73 | macroCode = macro.expansion 74 | } 75 | } 76 | } 77 | 78 | private func saveMacro() { 79 | // Validate input 80 | guard !macroName.trimmingCharacters(in: .whitespaces).isEmpty else { 81 | errorMessage = "Vui lòng nhập tên gõ tắt" 82 | showError = true 83 | return 84 | } 85 | 86 | guard !macroCode.trimmingCharacters(in: .whitespaces).isEmpty else { 87 | errorMessage = "Vui lòng nhập nội dung" 88 | showError = true 89 | return 90 | } 91 | 92 | // Load existing macros 93 | let defaults = UserDefaults.standard 94 | var macros = loadMacros() 95 | var trimmedName = macroName.trimmingCharacters(in: .whitespaces) 96 | var trimmedCode = macroCode.trimmingCharacters(in: .whitespaces) 97 | // Normalize Unicode to NFC to avoid duplicate variants and ensure stable encoding 98 | trimmedName = (trimmedName as NSString).precomposedStringWithCanonicalMapping 99 | trimmedCode = (trimmedCode as NSString).precomposedStringWithCanonicalMapping 100 | 101 | if isEditMode { 102 | guard let editingMacro else { 103 | errorMessage = "Không tìm thấy gõ tắt để chỉnh sửa" 104 | showError = true 105 | return 106 | } 107 | 108 | // EDIT MODE: Prefer matching by id to allow renaming 109 | let matchById = macros.firstIndex(where: { $0.id == editingMacro.id }) 110 | let matchByName = macros.firstIndex(where: { 111 | $0.shortcut.lowercased() == editingMacro.shortcut.lowercased() 112 | }) 113 | 114 | guard let index = matchById ?? matchByName else { 115 | errorMessage = "Không tìm thấy gõ tắt để chỉnh sửa" 116 | showError = true 117 | return 118 | } 119 | 120 | // Prevent duplicate names when renaming 121 | if trimmedName.lowercased() != editingMacro.shortcut.lowercased(), 122 | macros.contains(where: { 123 | $0.id != editingMacro.id && 124 | $0.shortcut.lowercased() == trimmedName.lowercased() 125 | }) { 126 | errorMessage = "Gõ tắt '\(trimmedName)' đã tồn tại" 127 | showError = true 128 | return 129 | } 130 | 131 | print("[MacroEditor] Editing macro: \(editingMacro.shortcut)") 132 | macros[index].shortcut = trimmedName 133 | macros[index].expansion = trimmedCode 134 | print("[MacroEditor] Updated to: \(trimmedName) -> \(trimmedCode)") 135 | } else { 136 | // ADD MODE: Check if macro already exists 137 | if macros.contains(where: { $0.shortcut.lowercased() == trimmedName.lowercased() }) { 138 | errorMessage = "Gõ tắt '\(trimmedName)' đã tồn tại" 139 | showError = true 140 | return 141 | } 142 | 143 | // Add new macro 144 | let newMacro = MacroItem( 145 | shortcut: trimmedName, 146 | expansion: trimmedCode) 147 | macros.append(newMacro) 148 | print("[MacroEditor] Added new macro: \(newMacro.shortcut) -> \(newMacro.expansion)") 149 | } 150 | 151 | // Sort macros by shortcut for stable order 152 | macros.sort { $0.shortcut.localizedCompare($1.shortcut) == .orderedAscending } 153 | 154 | // Save to UserDefaults atomically then notify immediately (no artificial delay) 155 | if let encoded = try? JSONEncoder().encode(macros) { 156 | defaults.set(encoded, forKey: "macroList") 157 | defaults.synchronize() 158 | print("[MacroEditor] Saved \(macros.count) macros to UserDefaults") 159 | print("[MacroEditor] macroList data size: \(encoded.count) bytes") 160 | 161 | // Post notification immediately; AppDelegate will rebuild macroData synchronously 162 | NotificationCenter.default.post(name: NSNotification.Name("MacrosUpdated"), object: nil) 163 | print("[MacroEditor] Notification posted") 164 | 165 | // Close the editor promptly 166 | isPresented = false 167 | } else { 168 | errorMessage = "Không thể lưu gõ tắt" 169 | showError = true 170 | print("[MacroEditor] ERROR: Failed to encode macros") 171 | } 172 | } 173 | 174 | private func loadMacros() -> [MacroItem] { 175 | let defaults = UserDefaults.standard 176 | if let data = defaults.data(forKey: "macroList"), 177 | let macros = try? JSONDecoder().decode([MacroItem].self, from: data) 178 | { 179 | return macros 180 | } 181 | return [] 182 | } 183 | } 184 | 185 | #Preview { 186 | @Previewable @State var isPresented = true 187 | return MacroEditorView(isPresented: $isPresented) 188 | } 189 | 190 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2019-2025 Phạm Hùng Tiến 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | ================================================================================ 20 | 21 | PREAMBLE 22 | 23 | This license applies to any program in the PHTV distribution that contains 24 | a notice placed by the copyright holder saying it may be distributed under 25 | the terms of this General Public License. 26 | 27 | PHTV - Vietnamese Input Method for macOS 28 | Project repository: https://github.com/PhamHungTien/PHTV 29 | OpenKey repository: https://github.com/tuyenvm/OpenKey 30 | 31 | ================================================================================ 32 | 33 | This program is based on the original OpenKey engine for Vietnamese text input. 34 | The engine components (Vietnamese.cpp, Engine.cpp, etc.) provide the core 35 | functionality for handling Vietnamese typing with various input methods 36 | (Telex, VNI, Simple Telex) and character encodings (Unicode, TCVN3, VNI). 37 | 38 | ================================================================================ 39 | 40 | TERMS AND CONDITIONS 41 | 42 | 0. Definitions. 43 | 44 | "This License" refers to version 3 of the GNU General Public License. 45 | 46 | "Copyright" also means copyright-like laws that apply to other kinds of works, 47 | such as semiconductor masks. 48 | 49 | "The Program" refers to any copyrightable work licensed under this License. 50 | Each licensee is addressed as "you". 51 | 52 | "Licensees" and "recipients" may be individuals or organizations. 53 | 54 | "To modify" a work means to copy from or adapt all or part of the work 55 | in a fashion requiring copyright permission, other than the making of an 56 | exact copy. The resulting work is called a "modified version" of the earlier 57 | work or a work "based on" the earlier work. 58 | 59 | "A covered work" means either the unmodified Program or a derivative work 60 | under copyright law. 61 | 62 | 1. Source Code. 63 | 64 | The "source code" for a work means the preferred form of the work for making 65 | modifications to it. 66 | 67 | 2. Basic Permissions. 68 | 69 | All rights granted under this License are granted for the term of copyright 70 | on the Program, and are irrevocable provided the stated conditions are met. 71 | You are granted the rights as follows: 72 | 73 | a) You may run the unmodified Program. You are free to run the Program 74 | for any purpose. 75 | 76 | b) You may make and run privately modified versions of the Program. 77 | 78 | c) You may distribute copies of the Program as received, in source or binary 79 | form, under the terms of Section 4, provided you also meet all of these 80 | conditions: 81 | 82 | i) The work must carry prominent notices stating that you modified it. 83 | 84 | ii) The work must carry a notice of the GPL v3 license and this notice. 85 | 86 | iii) You must license the entire work under this License. 87 | 88 | iv) If the Program normally reads commands interactively, you must make 89 | sure that when it starts in a normal mode of operation, it prints a notice 90 | including an appropriate copyright notice and a notice that there is no 91 | warranty. 92 | 93 | d) You may distribute the Program (including any work based on the Program) 94 | under the terms of Section 4. 95 | 96 | 3. Copyleft. 97 | 98 | Each time you distribute the Program (or any work based on the Program), 99 | the recipient automatically receives a license from the original licensor 100 | to run, modify and distribute the Program subject to these terms and conditions. 101 | 102 | 4. Distribution. 103 | 104 | You may distribute copies of source code of the Program you receive or 105 | modified versions of the Program under the terms of Sections 3 and 4, 106 | provided that you also meet all of these conditions: 107 | 108 | a) The work must carry prominent notices stating that you modified it, and 109 | giving a relevant date. 110 | 111 | b) The work must carry prominent notices stating it is released under this 112 | License and any conditions added under section 7. This requirement modifies 113 | the requirement in section 4 to "keep intact all notices". 114 | 115 | c) You must license the entire work, as a whole, under this License to anyone 116 | who comes into possession of a copy. This License will therefore apply, along 117 | with any applicable section 7 additional terms, to the whole of the work, 118 | and all its parts, regardless of how they are packaged. 119 | 120 | d) Notwithstanding any other provision of this License, you have permission 121 | to link or combine any covered work with a work licensed under version 3 122 | or later of the GNU Affero General Public License into a single combined work, 123 | and to convey the resulting work. 124 | 125 | e) If you distribute executable or object code versions of the Program 126 | (or works based on them), you must offer to distribute the source code 127 | of the Program. 128 | 129 | 5. Non-Source Distribution. 130 | 131 | You may distribute a non-source version of a work based on the Program 132 | under Sections 4 and 5, provided that you also do one of the following: 133 | 134 | a) Accompany it with the complete corresponding machine-readable source code 135 | under Sections 4 and 5. 136 | 137 | b) Accompany it with a written offer, which is valid for at least three years 138 | and valid for as long as you offer spare parts or customer support for that 139 | model, to give anyone who possesses the object code either (1) a copy of the 140 | corresponding source code for all the software in the object code covered by 141 | this License, for a price no more than your reasonable cost of physically 142 | performing this source distribution, or (2) access to copy the corresponding 143 | source code from a network server at no charge. 144 | 145 | 6. Patent Grant. 146 | 147 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent 148 | license under the contributor's patent claims, to make, have made, use, offer 149 | to sell, sell, import, and otherwise transfer the contribution. 150 | 151 | 7. No Warranty. 152 | 153 | THE PROGRAM IS PROVIDED WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 154 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR 155 | A PARTICULAR PURPOSE AND NONINFRINGEMENT. 156 | 157 | 8. Limitation of Liability. 158 | 159 | IN NO EVENT SHALL ANY COPYRIGHT HOLDER OR OTHER PARTY PROVIDE MODIFICATIONS 160 | TO THE PROGRAM BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, 161 | INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO 162 | USE THE PROGRAM. 163 | 164 | ================================================================================ 165 | 166 | This project uses and adapts code and concepts from the OpenKey Vietnamese 167 | input method engine developed by Tuyến Võ Minh. PHTV (Phạm Hùng Tiến Vietnamese) 168 | extends this engine with modern macOS integration, SwiftUI interface, and additional 169 | features while maintaining GPL-3.0 compliance. 170 | 171 | OpenKey: https://github.com/tuyenvm/OpenKey 172 | 173 | For more information about GPL-3.0, visit: https://www.gnu.org/licenses/gpl-3.0.html 174 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Views/Settings/AboutView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct AboutView: View { 12 | @EnvironmentObject var themeManager: ThemeManager 13 | 14 | var body: some View { 15 | ScrollView { 16 | VStack(spacing: 24) { 17 | // App Icon and Name 18 | VStack(spacing: 16) { 19 | ZStack { 20 | if #available(macOS 26.0, *) { 21 | RoundedRectangle(cornerRadius: 28) 22 | .fill( 23 | LinearGradient( 24 | colors: [ 25 | themeManager.themeColor.opacity(0.12), 26 | themeManager.themeColor.opacity(0.08), 27 | ], 28 | startPoint: .topLeading, 29 | endPoint: .bottomTrailing 30 | ) 31 | ) 32 | .frame(width: 120, height: 120) 33 | .glassEffect(in: .rect(cornerRadius: 28)) 34 | } else { 35 | RoundedRectangle(cornerRadius: 28) 36 | .fill( 37 | LinearGradient( 38 | colors: [ 39 | themeManager.themeColor.opacity(0.2), 40 | themeManager.themeColor.opacity(0.15), 41 | ], 42 | startPoint: .topLeading, 43 | endPoint: .bottomTrailing 44 | ) 45 | ) 46 | .frame(width: 120, height: 120) 47 | } 48 | 49 | AppIconView() 50 | .frame(width: 100, height: 100) 51 | .clipShape(RoundedRectangle(cornerRadius: 22)) 52 | .shadow(color: .black.opacity(0.2), radius: 8, x: 0, y: 4) 53 | } 54 | 55 | VStack(spacing: 8) { 56 | Text("PHTV") 57 | .font(.system(size: 32, weight: .bold, design: .rounded)) 58 | 59 | Text("Bộ gõ tiếng Việt cho macOS") 60 | .font(.title3) 61 | .foregroundStyle(.secondary) 62 | } 63 | 64 | // Version Badge 65 | HStack(spacing: 8) { 66 | Text("Phiên bản") 67 | .font(.caption) 68 | .foregroundStyle(.secondary) 69 | 70 | Text( 71 | "v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.1.0")" 72 | ) 73 | .font(.system(.caption, design: .monospaced)) 74 | .fontWeight(.semibold) 75 | .padding(.horizontal, 8) 76 | .padding(.vertical, 4) 77 | .background(Capsule().fill(themeManager.themeColor.opacity(0.15))) 78 | .foregroundStyle(themeManager.themeColor) 79 | } 80 | } 81 | .padding(.top, 20) 82 | 83 | Divider() 84 | .padding(.horizontal, 40) 85 | 86 | // Developer Info 87 | VStack(spacing: 16) { 88 | AboutInfoCard( 89 | icon: "person.circle.fill", 90 | iconColor: themeManager.themeColor, 91 | title: "Phát triển bởi", 92 | value: "Phạm Hùng Tiến" 93 | ) 94 | 95 | AboutInfoCard( 96 | icon: "calendar.circle.fill", 97 | iconColor: themeManager.themeColor, 98 | title: "Phát hành", 99 | value: "2026" 100 | ) 101 | 102 | AboutInfoCard( 103 | icon: "swift", 104 | iconColor: themeManager.themeColor, 105 | title: "Công nghệ", 106 | value: "Swift & SwiftUI" 107 | ) 108 | } 109 | .padding(.horizontal, 20) 110 | 111 | Divider() 112 | .padding(.horizontal, 40) 113 | 114 | // Support Section 115 | VStack(spacing: 16) { 116 | Text("Ủng hộ phát triển") 117 | .font(.headline) 118 | .foregroundStyle(.primary) 119 | 120 | Text( 121 | "Nếu bạn thấy PHTV hữu ích, hãy ủng hộ để giúp phát triển thêm các tính năng mới" 122 | ) 123 | .font(.subheadline) 124 | .foregroundStyle(.secondary) 125 | .multilineTextAlignment(.center) 126 | .padding(.horizontal, 20) 127 | 128 | if let donateImage = NSImage(named: "donate") { 129 | VStack(spacing: 8) { 130 | Image(nsImage: donateImage) 131 | .resizable() 132 | .scaledToFit() 133 | .frame(maxWidth: 220) 134 | .clipShape(RoundedRectangle(cornerRadius: 12)) 135 | .shadow(color: .black.opacity(0.1), radius: 4, x: 0, y: 2) 136 | 137 | Text("Quét mã để ủng hộ") 138 | .font(.caption) 139 | .foregroundStyle(.secondary) 140 | } 141 | .padding(16) 142 | .background( 143 | RoundedRectangle(cornerRadius: 16) 144 | .fill(Color(NSColor.controlBackgroundColor)) 145 | ) 146 | } 147 | } 148 | .padding(.horizontal, 20) 149 | 150 | Spacer(minLength: 20) 151 | 152 | // Footer 153 | VStack(spacing: 6) { 154 | Text("Copyright © 2026 Phạm Hùng Tiến") 155 | .font(.caption) 156 | .foregroundStyle(.secondary) 157 | 158 | Text("All rights reserved") 159 | .font(.caption2) 160 | .foregroundStyle(.tertiary) 161 | } 162 | .padding(.bottom, 20) 163 | } 164 | } 165 | .background(Color(NSColor.windowBackgroundColor)) 166 | } 167 | } 168 | 169 | struct AboutInfoCard: View { 170 | let icon: String 171 | let iconColor: Color 172 | let title: String 173 | let value: String 174 | 175 | var body: some View { 176 | HStack(spacing: 14) { 177 | ZStack { 178 | if #available(macOS 26.0, *) { 179 | RoundedRectangle(cornerRadius: 10) 180 | .fill(iconColor.opacity(0.08)) 181 | .frame(width: 42, height: 42) 182 | .glassEffect(in: .rect(cornerRadius: 10)) 183 | } else { 184 | RoundedRectangle(cornerRadius: 10) 185 | .fill(iconColor.opacity(0.12)) 186 | .frame(width: 42, height: 42) 187 | } 188 | 189 | Image(systemName: icon) 190 | .font(.system(size: 18, weight: .medium)) 191 | .foregroundStyle(iconColor) 192 | } 193 | 194 | VStack(alignment: .leading, spacing: 2) { 195 | Text(title) 196 | .font(.caption) 197 | .foregroundStyle(.secondary) 198 | 199 | Text(value) 200 | .font(.body) 201 | .fontWeight(.medium) 202 | .foregroundStyle(.primary) 203 | } 204 | 205 | Spacer() 206 | } 207 | .padding(14) 208 | .background { 209 | if #available(macOS 26.0, *) { 210 | RoundedRectangle(cornerRadius: 12) 211 | .fill(.white.opacity(0.08)) 212 | .glassEffect(in: .rect(cornerRadius: 12)) 213 | } else { 214 | RoundedRectangle(cornerRadius: 12) 215 | .fill(Color(NSColor.controlBackgroundColor)) 216 | } 217 | } 218 | } 219 | } 220 | 221 | // MARK: - App Icon View 222 | private struct AppIconView: View { 223 | var body: some View { 224 | if let iconPath = Bundle.main.path(forResource: "Icon", ofType: "icns"), 225 | let icon = NSImage(contentsOfFile: iconPath) 226 | { 227 | Image(nsImage: icon) 228 | .resizable() 229 | .scaledToFit() 230 | } else if let icon = NSApp.applicationIconImage { 231 | Image(nsImage: icon) 232 | .resizable() 233 | .scaledToFit() 234 | } else if let icon = NSImage(named: NSImage.applicationIconName) { 235 | Image(nsImage: icon) 236 | .resizable() 237 | .scaledToFit() 238 | } else { 239 | // Fallback 240 | Image(systemName: "square.fill") 241 | .font(.system(size: 50)) 242 | .foregroundStyle(.tint) 243 | } 244 | } 245 | } 246 | 247 | #Preview { 248 | AboutView() 249 | .environmentObject(ThemeManager.shared) 250 | .frame(width: 500, height: 700) 251 | } 252 | -------------------------------------------------------------------------------- /PHTV/SwiftUI/Controllers/StatusBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusBarController.swift 3 | // PHTV 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AppKit 11 | 12 | @MainActor 13 | class StatusBarController: ObservableObject { 14 | private var statusItem: NSStatusItem? 15 | @Published var currentInputMethod: String = "VN" 16 | @Published var currentCodeTable: String = "Unicode" 17 | @Published var isEnabled: Bool = true 18 | private var menuBarIconSize: Double = 18.0 19 | 20 | init() { 21 | setupStatusItem() 22 | setupNotificationObservers() 23 | } 24 | 25 | private func setupStatusItem() { 26 | statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 27 | 28 | if let button = statusItem?.button { 29 | updateStatusButton() 30 | button.action = #selector(statusBarButtonClicked) 31 | button.target = self 32 | button.sendAction(on: [.leftMouseUp, .rightMouseUp]) 33 | } 34 | 35 | setupMenu() 36 | } 37 | 38 | @objc private func statusBarButtonClicked(sender: NSStatusBarButton) { 39 | guard let event = NSApp.currentEvent else { return } 40 | 41 | if event.type == .rightMouseUp { 42 | statusItem?.menu = createMenu() 43 | statusItem?.button?.performClick(nil) 44 | statusItem?.menu = nil 45 | } else { 46 | // Left click - show settings 47 | showSettings() 48 | } 49 | } 50 | 51 | private func setupMenu() { 52 | // Menu will be created dynamically on right-click 53 | } 54 | 55 | private func createMenu() -> NSMenu { 56 | let menu = NSMenu() 57 | 58 | // Status section 59 | let statusItem = NSMenuItem(title: isEnabled ? "Đang bật" : "Đang tắt", action: nil, keyEquivalent: "") 60 | statusItem.isEnabled = false 61 | menu.addItem(statusItem) 62 | 63 | menu.addItem(NSMenuItem.separator()) 64 | 65 | // Input Method section 66 | let inputMethodMenu = NSMenu() 67 | let inputMethods = [ 68 | ("Telex", 0), 69 | ("VNI", 1), 70 | ("Simple Telex 1", 2), 71 | ("Simple Telex 2", 3) 72 | ] 73 | 74 | for (name, tag) in inputMethods { 75 | let item = NSMenuItem(title: name, action: #selector(selectInputMethod(_:)), keyEquivalent: "") 76 | item.tag = tag 77 | item.target = self 78 | item.state = (tag == 0) ? .on : .off 79 | inputMethodMenu.addItem(item) 80 | } 81 | 82 | let inputMethodItem = NSMenuItem(title: "Kiểu gõ", action: nil, keyEquivalent: "") 83 | inputMethodItem.submenu = inputMethodMenu 84 | menu.addItem(inputMethodItem) 85 | 86 | // Code Table section 87 | let codeTableMenu = NSMenu() 88 | let codeTables = [ 89 | ("Unicode", 0), 90 | ("TCVN3", 1), 91 | ("VNI Windows", 2), 92 | ("Unicode Composite", 3), 93 | ("Vietnamese Locale (CP1258)", 4) 94 | ] 95 | 96 | for (name, tag) in codeTables { 97 | let item = NSMenuItem(title: name, action: #selector(selectCodeTable(_:)), keyEquivalent: "") 98 | item.tag = tag 99 | item.target = self 100 | item.state = (tag == 0) ? .on : .off 101 | codeTableMenu.addItem(item) 102 | } 103 | 104 | let codeTableItem = NSMenuItem(title: "Bảng mã", action: nil, keyEquivalent: "") 105 | codeTableItem.submenu = codeTableMenu 106 | menu.addItem(codeTableItem) 107 | 108 | menu.addItem(NSMenuItem.separator()) 109 | 110 | // Quick actions 111 | menu.addItem(NSMenuItem(title: "Tạm tắt (\(getHotkeyString()))", action: #selector(toggleEnabled), keyEquivalent: "")) 112 | menu.addItem(NSMenuItem(title: "Công cụ chuyển mã", action: #selector(showConvertTool), keyEquivalent: "")) 113 | 114 | menu.addItem(NSMenuItem.separator()) 115 | 116 | // Settings 117 | menu.addItem(NSMenuItem(title: "Cài đặt...", action: #selector(showSettingsMenu), keyEquivalent: ",")) 118 | menu.addItem(NSMenuItem(title: "Về PHTV...", action: #selector(showAbout), keyEquivalent: "")) 119 | 120 | menu.addItem(NSMenuItem.separator()) 121 | 122 | // Quit 123 | menu.addItem(NSMenuItem(title: "Thoát PHTV", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) 124 | 125 | return menu 126 | } 127 | 128 | @objc private func selectInputMethod(_ sender: NSMenuItem) { 129 | // Update input method 130 | let methods = ["Telex", "VNI", "Simple Telex 1", "Simple Telex 2"] 131 | currentInputMethod = methods[sender.tag] 132 | updateStatusButton() 133 | 134 | // Notify backend (will bridge to Objective-C) 135 | NotificationCenter.default.post(name: NSNotification.Name("InputMethodChanged"), object: sender.tag) 136 | } 137 | 138 | @objc private func selectCodeTable(_ sender: NSMenuItem) { 139 | // Update code table 140 | let tables = ["Unicode", "TCVN3", "VNI Windows", "Unicode Composite", "CP1258"] 141 | currentCodeTable = tables[sender.tag] 142 | 143 | // Notify backend 144 | NotificationCenter.default.post(name: NSNotification.Name("CodeTableChanged"), object: sender.tag) 145 | } 146 | 147 | @objc private func toggleEnabled() { 148 | isEnabled.toggle() 149 | updateStatusButton() 150 | 151 | // Notify backend 152 | NotificationCenter.default.post(name: NSNotification.Name("ToggleEnabled"), object: isEnabled) 153 | } 154 | 155 | @objc private func showConvertTool() { 156 | NotificationCenter.default.post(name: NSNotification.Name("ShowConvertTool"), object: nil) 157 | } 158 | 159 | @objc private func showSettingsMenu() { 160 | showSettings() 161 | } 162 | 163 | @objc private func showAbout() { 164 | NotificationCenter.default.post(name: NSNotification.Name("ShowAbout"), object: nil) 165 | } 166 | 167 | private func showSettings() { 168 | // Open settings window 169 | if #available(macOS 14.0, *) { 170 | NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) 171 | } else { 172 | NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) 173 | } 174 | } 175 | 176 | func updateStatusButton() { 177 | guard let button = statusItem?.button else { return } 178 | 179 | let size = CGFloat(menuBarIconSize) 180 | if let image = makeMenuBarIcon(size: size, slashed: !isEnabled) { 181 | image.isTemplate = true 182 | image.size = NSSize(width: size, height: size) 183 | button.image = image 184 | button.title = "" 185 | button.imagePosition = .imageOnly 186 | } 187 | 188 | // Update tooltip 189 | button.toolTip = "PHTV - \(currentInputMethod) (\(currentCodeTable))\n\(isEnabled ? "Đang bật" : "Đang tắt")" 190 | } 191 | 192 | private func makeMenuBarIcon(size: CGFloat, slashed: Bool) -> NSImage? { 193 | // Use different icons based on language mode 194 | let baseIcon: NSImage? = { 195 | if slashed { 196 | // English mode - use menubar_english.png 197 | if let englishIcon = NSImage(named: "menubar_english") { 198 | return englishIcon 199 | } 200 | } 201 | // Vietnamese mode - use menubar_vietnamese.png or menubar_icon.png based on preference 202 | let useVietnameseIcon = AppState.shared.useVietnameseMenubarIcon 203 | if useVietnameseIcon, let vietnameseIcon = NSImage(named: "menubar_vietnamese") { 204 | return vietnameseIcon 205 | } 206 | if let img = NSImage(named: "menubar_icon") { 207 | return img 208 | } 209 | return NSApplication.shared.applicationIconImage 210 | }() 211 | guard let baseIcon else { return nil } 212 | let targetSize = NSSize(width: size, height: size) 213 | let img = NSImage(size: targetSize) 214 | img.lockFocus() 215 | defer { img.unlockFocus() } 216 | 217 | // Draw app icon scaled to fit (no blurring) 218 | let rect = NSRect(origin: .zero, size: targetSize) 219 | baseIcon.draw(in: rect, from: .zero, operation: .sourceOver, fraction: 1.0, respectFlipped: true, hints: [.interpolation: NSImageInterpolation.high]) 220 | 221 | return img 222 | } 223 | 224 | private func setupNotificationObservers() { 225 | NotificationCenter.default.addObserver( 226 | forName: NSNotification.Name("MenuBarIconSizeChanged"), 227 | object: nil, 228 | queue: .main 229 | ) { [weak self] notification in 230 | guard let self = self else { return } 231 | if let size = notification.object as? NSNumber { 232 | Task { @MainActor in 233 | self.menuBarIconSize = size.doubleValue 234 | self.updateStatusButton() 235 | } 236 | } 237 | } 238 | 239 | NotificationCenter.default.addObserver( 240 | forName: NSNotification.Name("MenuBarIconPreferenceChanged"), 241 | object: nil, 242 | queue: .main 243 | ) { [weak self] _ in 244 | guard let self = self else { return } 245 | Task { @MainActor in 246 | self.updateStatusButton() 247 | } 248 | } 249 | } 250 | 251 | private func getHotkeyString() -> String { 252 | // Get hotkey from AppState 253 | let appState = AppState.shared 254 | var parts: [String] = [] 255 | if appState.switchKeyCommand { parts.append("⌘") } 256 | if appState.switchKeyOption { parts.append("⌥") } 257 | if appState.switchKeyControl { parts.append("⌃") } 258 | if appState.switchKeyShift { parts.append("⇧") } 259 | parts.append("Z") 260 | return parts.joined() 261 | } 262 | } 263 | 264 | // MARK: - SwiftUI Integration 265 | struct StatusBarView: View { 266 | @StateObject private var controller = StatusBarController() 267 | 268 | var body: some View { 269 | EmptyView() 270 | .onAppear { 271 | // Status bar is managed by the controller 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /PHTVFunctionalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PHTVFunctionalTests.swift 3 | // PHTV Tests 4 | // 5 | // Created by Phạm Hùng Tiến on 2026. 6 | // Copyright © 2026 Phạm Hùng Tiến. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | // MARK: - Test Suite cho các cải tiến 12 | 13 | class PHTVFunctionalTests: XCTestCase { 14 | 15 | override func setUp() { 16 | super.setUp() 17 | // Clear UserDefaults before each test 18 | let domain = Bundle.main.bundleIdentifier ?? "" 19 | UserDefaults.standard.removePersistentDomain(forName: domain) 20 | UserDefaults.standard.synchronize() 21 | } 22 | 23 | override func tearDown() { 24 | super.tearDown() 25 | // Clean up 26 | UserDefaults.standard.synchronize() 27 | } 28 | 29 | // MARK: - Test 1: Macro Hot-Reload 30 | /// Test Case: Thêm macro mới và kiểm tra nó được lưu ngay lập tức 31 | func testMacroHotReload() { 32 | let testMacroName = "tvn" 33 | let testMacroContent = "Tiếng Việt Nam" 34 | 35 | // 1. Simulate saving macro 36 | var macros: [MacroItem] = [] 37 | let newMacro = MacroItem(shortcut: testMacroName, expansion: testMacroContent) 38 | macros.append(newMacro) 39 | 40 | if let encoded = try? JSONEncoder().encode(macros) { 41 | UserDefaults.standard.set(encoded, forKey: "macroList") 42 | UserDefaults.standard.synchronize() 43 | } 44 | 45 | // 2. Verify it's saved 46 | if let data = UserDefaults.standard.data(forKey: "macroList"), 47 | let loadedMacros = try? JSONDecoder().decode([MacroItem].self, from: data) 48 | { 49 | XCTAssertEqual(loadedMacros.count, 1) 50 | XCTAssertEqual(loadedMacros[0].shortcut, testMacroName) 51 | XCTAssertEqual(loadedMacros[0].expansion, testMacroContent) 52 | print("✅ Test 1 PASS: Macro hot-reload works correctly") 53 | } else { 54 | XCTFail("Failed to load saved macros") 55 | } 56 | } 57 | 58 | // MARK: - Test 2: Settings Observer Debouncing 59 | /// Test Case: SettingsObserver không gọi update quá tần suất 60 | func testSettingsObserverDebouncing() { 61 | let observer = SettingsObserver.shared 62 | var updateCount = 0 63 | let expectation = XCTestExpectation(description: "Debounce completes") 64 | 65 | // Subscribe to changes 66 | var cancellable: NSObjectProtocol? 67 | cancellable = NotificationCenter.default.addObserver( 68 | forName: UserDefaults.didChangeNotification, 69 | object: UserDefaults.standard, 70 | queue: .main 71 | ) { _ in 72 | updateCount += 1 73 | } 74 | 75 | // Make rapid changes 76 | for i in 0..<5 { 77 | UserDefaults.standard.set(i, forKey: "TestKey_\(i)") 78 | } 79 | 80 | // Wait for debounce 81 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 82 | expectation.fulfill() 83 | } 84 | 85 | wait(for: [expectation], timeout: 1.0) 86 | 87 | if let cancellable = cancellable { 88 | NotificationCenter.default.removeObserver(cancellable) 89 | } 90 | 91 | // Should have multiple updates but debounced 92 | XCTAssertGreater(updateCount, 0) 93 | print("✅ Test 2 PASS: Settings observer debouncing works (updates: \(updateCount))") 94 | } 95 | 96 | // MARK: - Test 3: Excluded Apps Management 97 | /// Test Case: Thêm/xóa ứng dụng loại trừ hoạt động đúng 98 | func testExcludedAppsManagement() { 99 | // 1. Create test app 100 | let testApp = ExcludedApp( 101 | bundleIdentifier: "com.test.app", 102 | name: "Test App", 103 | path: "/Applications/TestApp.app" 104 | ) 105 | 106 | // 2. Save to defaults 107 | var apps: [ExcludedApp] = [testApp] 108 | if let encoded = try? JSONEncoder().encode(apps) { 109 | UserDefaults.standard.set(encoded, forKey: "ExcludedApps") 110 | UserDefaults.standard.synchronize() 111 | } 112 | 113 | // 3. Load and verify 114 | if let data = UserDefaults.standard.data(forKey: "ExcludedApps"), 115 | let loadedApps = try? JSONDecoder().decode([ExcludedApp].self, from: data) 116 | { 117 | XCTAssertEqual(loadedApps.count, 1) 118 | XCTAssertEqual(loadedApps[0].bundleIdentifier, "com.test.app") 119 | XCTAssertEqual(loadedApps[0].name, "Test App") 120 | print("✅ Test 3 PASS: Excluded apps management works correctly") 121 | } else { 122 | XCTFail("Failed to load excluded apps") 123 | } 124 | } 125 | 126 | // MARK: - Test 4: Settings Persistence 127 | /// Test Case: Cài đặt được lưu và load chính xác 128 | func testSettingsPersistence() { 129 | // Save various settings 130 | UserDefaults.standard.set(0, forKey: "InputType") // Telex 131 | UserDefaults.standard.set(1, forKey: "InputMethod") // Vietnamese enabled 132 | UserDefaults.standard.set(true, forKey: "Spelling") 133 | UserDefaults.standard.set(true, forKey: "UseMacro") 134 | UserDefaults.standard.synchronize() 135 | 136 | // Load and verify 137 | let inputType = UserDefaults.standard.integer(forKey: "InputType") 138 | let inputMethod = UserDefaults.standard.integer(forKey: "InputMethod") 139 | let spelling = UserDefaults.standard.bool(forKey: "Spelling") 140 | let useMacro = UserDefaults.standard.bool(forKey: "UseMacro") 141 | 142 | XCTAssertEqual(inputType, 0) 143 | XCTAssertEqual(inputMethod, 1) 144 | XCTAssertTrue(spelling) 145 | XCTAssertTrue(useMacro) 146 | print("✅ Test 4 PASS: Settings persistence works correctly") 147 | } 148 | 149 | // MARK: - Test 5: Default Settings Reset 150 | /// Test Case: Reset to defaults hoạt động 151 | func testDefaultSettingsReset() { 152 | // Save custom settings 153 | UserDefaults.standard.set(1, forKey: "InputType") // VNI 154 | UserDefaults.standard.set(false, forKey: "UseMacro") 155 | UserDefaults.standard.synchronize() 156 | 157 | // Verify custom settings are saved 158 | XCTAssertEqual(UserDefaults.standard.integer(forKey: "InputType"), 1) 159 | XCTAssertFalse(UserDefaults.standard.bool(forKey: "UseMacro")) 160 | 161 | // Reset to defaults 162 | let defaultSettings: [String: Any] = [ 163 | "InputType": 0, // Telex 164 | "InputMethod": 1, // Vietnamese 165 | "UseMacro": true, 166 | "Spelling": true, 167 | "UseSmartSwitchKey": true, 168 | ] 169 | 170 | for (key, value) in defaultSettings { 171 | UserDefaults.standard.set(value, forKey: key) 172 | } 173 | UserDefaults.standard.synchronize() 174 | 175 | // Verify reset worked 176 | XCTAssertEqual(UserDefaults.standard.integer(forKey: "InputType"), 0) 177 | XCTAssertTrue(UserDefaults.standard.bool(forKey: "UseMacro")) 178 | print("✅ Test 5 PASS: Reset to defaults works correctly") 179 | } 180 | 181 | // MARK: - Test 6: Hotkey Settings 182 | /// Test Case: Hotkey settings được encode/decode đúng 183 | func testHotkeySettings() { 184 | // Simulate hotkey: Ctrl + Shift (0xFE = no key, just modifiers) 185 | var status = 0xFE // No key 186 | status |= 0x100 // Control 187 | status |= 0x800 // Shift 188 | 189 | UserDefaults.standard.set(status, forKey: "SwitchKeyStatus") 190 | UserDefaults.standard.synchronize() 191 | 192 | // Load and verify 193 | let loadedStatus = UserDefaults.standard.integer(forKey: "SwitchKeyStatus") 194 | 195 | let hasControl = (loadedStatus & 0x100) != 0 196 | let hasShift = (loadedStatus & 0x800) != 0 197 | let keyCode = UInt16(loadedStatus & 0xFF) 198 | 199 | XCTAssertTrue(hasControl) 200 | XCTAssertTrue(hasShift) 201 | XCTAssertEqual(keyCode, 0xFE) 202 | print("✅ Test 6 PASS: Hotkey settings encode/decode works correctly") 203 | } 204 | 205 | // MARK: - Test 7: Macro List Operations 206 | /// Test Case: Thêm, xóa, chỉnh sửa macro 207 | func testMacroListOperations() { 208 | var macros: [MacroItem] = [] 209 | 210 | // Add 3 macros 211 | macros.append(MacroItem(shortcut: "tvn", expansion: "Tiếng Việt Nam")) 212 | macros.append(MacroItem(shortcut: "hnt", expansion: "Hùng Tiến")) 213 | macros.append(MacroItem(shortcut: "phtv", expansion: "PHTV Bộ gõ")) 214 | 215 | // Save 216 | if let encoded = try? JSONEncoder().encode(macros) { 217 | UserDefaults.standard.set(encoded, forKey: "macroList") 218 | UserDefaults.standard.synchronize() 219 | } 220 | 221 | // Load 222 | if let data = UserDefaults.standard.data(forKey: "macroList"), 223 | let loadedMacros = try? JSONDecoder().decode([MacroItem].self, from: data) 224 | { 225 | XCTAssertEqual(loadedMacros.count, 3) 226 | 227 | // Remove one 228 | var updatedMacros = loadedMacros 229 | updatedMacros.removeAll { $0.shortcut == "hnt" } 230 | XCTAssertEqual(updatedMacros.count, 2) 231 | 232 | // Edit one 233 | if var editMacro = updatedMacros.first(where: { $0.shortcut == "tvn" }) { 234 | editMacro.expansion = "Việt Nam (Updated)" 235 | updatedMacros.removeAll { $0.shortcut == "tvn" } 236 | updatedMacros.append(editMacro) 237 | } 238 | XCTAssertEqual(updatedMacros.count, 2) 239 | 240 | print("✅ Test 7 PASS: Macro list operations work correctly") 241 | } else { 242 | XCTFail("Failed to load macros") 243 | } 244 | } 245 | 246 | // MARK: - Test 8: Performance Test 247 | /// Test Case: Kiểm tra hiệu suất lưu/load 248 | func testPerformance() { 249 | let startTime = Date() 250 | 251 | // Create 100 macros 252 | var macros: [MacroItem] = [] 253 | for i in 0..<100 { 254 | macros.append(MacroItem(shortcut: "m\(i)", expansion: "Macro \(i)")) 255 | } 256 | 257 | // Save 258 | if let encoded = try? JSONEncoder().encode(macros) { 259 | UserDefaults.standard.set(encoded, forKey: "macroList") 260 | UserDefaults.standard.synchronize() 261 | } 262 | 263 | // Load 264 | if let data = UserDefaults.standard.data(forKey: "macroList"), 265 | let loadedMacros = try? JSONDecoder().decode([MacroItem].self, from: data) 266 | { 267 | XCTAssertEqual(loadedMacros.count, 100) 268 | } 269 | 270 | let elapsed = Date().timeIntervalSince(startTime) 271 | XCTAssertLessThan(elapsed, 1.0) // Should complete in less than 1 second 272 | print("✅ Test 8 PASS: Performance test (\(String(format: "%.3f", elapsed))s)") 273 | } 274 | } 275 | 276 | // MARK: - Test Model 277 | struct MacroItem: Codable, Identifiable { 278 | var id: UUID = UUID() 279 | var shortcut: String 280 | var expansion: String 281 | 282 | enum CodingKeys: String, CodingKey { 283 | case id, shortcut, expansion 284 | } 285 | } 286 | 287 | struct ExcludedApp: Codable, Identifiable, Hashable { 288 | var id: String { bundleIdentifier } 289 | let bundleIdentifier: String 290 | let name: String 291 | let path: String 292 | 293 | init(bundleIdentifier: String, name: String, path: String) { 294 | self.bundleIdentifier = bundleIdentifier 295 | self.name = name 296 | self.path = path 297 | } 298 | } 299 | --------------------------------------------------------------------------------