├── img ├── donate.png ├── preview.png ├── preview_zh.png ├── preview_dark.png └── preview_zh_dark.png ├── xHistory ├── Assets.xcassets │ ├── Contents.json │ ├── Colors │ │ ├── Contents.json │ │ ├── buttonBlue.colorset │ │ │ └── Contents.json │ │ ├── buttonRed.colorset │ │ │ └── Contents.json │ │ ├── buttonGreen.colorset │ │ │ └── Contents.json │ │ ├── buttonYellow.colorset │ │ │ └── Contents.json │ │ ├── background.colorset │ │ │ └── Contents.json │ │ └── background2.colorset │ │ │ └── Contents.json │ ├── Menubar │ │ ├── Contents.json │ │ ├── menuBar.imageset │ │ │ ├── menuBarIcon.png │ │ │ ├── menuBarIcon@1x.png │ │ │ └── Contents.json │ │ └── menuBarInvert.imageset │ │ │ ├── menuBarIconInvert.png │ │ │ ├── menuBarIconInvert@1x.png │ │ │ └── Contents.json │ ├── Others │ │ ├── Contents.json │ │ └── textformat.imageset │ │ │ ├── Contents.json │ │ │ └── textformat.svg │ ├── Settings │ │ ├── Contents.json │ │ ├── gear.imageset │ │ │ ├── gear.png │ │ │ ├── gear@1x.png │ │ │ └── Contents.json │ │ ├── cloud.imageset │ │ │ ├── cloud.png │ │ │ ├── cloud@1x.png │ │ │ └── Contents.json │ │ ├── shell.imageset │ │ │ ├── shell.png │ │ │ ├── shellN.png │ │ │ ├── shell@1x.png │ │ │ ├── shell@1xN.png │ │ │ └── Contents.json │ │ ├── block.imageset │ │ │ ├── blacklist.png │ │ │ ├── blacklist@1x.png │ │ │ └── Contents.json │ │ ├── history.imageset │ │ │ ├── history.png │ │ │ ├── history@1x.png │ │ │ └── Contents.json │ │ └── hotkey.imageset │ │ │ ├── hotkey.png │ │ │ ├── hotkey@1x.png │ │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── icon_16x16.png │ │ ├── icon_32x32.png │ │ ├── icon_128x128.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_512x512@2x.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── xHistory.entitlements ├── Info.plist ├── Base.lproj │ └── Credits.rtf ├── zh-Hans.lproj │ ├── Credits.rtf │ └── Localizable.strings ├── zh-Hant.lproj │ ├── Credits.rtf │ └── Localizable.strings ├── Supports │ ├── CommandLineTool.swift │ ├── WindowAccessor.swift │ ├── Sparkle.swift │ ├── GroupForm.swift │ ├── SyntaxHighlighter.swift │ └── HistoryCopyer.swift ├── xHistoryApp.swift └── ViewModel │ ├── SettingsView.swift │ └── ContentView.swift ├── xHistory.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ ├── xh.xcscheme │ │ └── xHistory.xcscheme └── project.pbxproj ├── xh ├── MakeOverlay.swift └── main.swift ├── README_zh.md ├── .gitignore ├── README.md └── appcast.xml /img/donate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/img/donate.png -------------------------------------------------------------------------------- /img/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/img/preview.png -------------------------------------------------------------------------------- /img/preview_zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/img/preview_zh.png -------------------------------------------------------------------------------- /img/preview_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/img/preview_dark.png -------------------------------------------------------------------------------- /img/preview_zh_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/img/preview_zh_dark.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Menubar/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Others/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /xHistory/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/gear.imageset/gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/gear.imageset/gear.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/cloud.imageset/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/cloud.imageset/cloud.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/gear.imageset/gear@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/gear.imageset/gear@1x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/shell.imageset/shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/shell.imageset/shell.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/shell.imageset/shellN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/shell.imageset/shellN.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/block.imageset/blacklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/block.imageset/blacklist.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/cloud.imageset/cloud@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/cloud.imageset/cloud@1x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/history.imageset/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/history.imageset/history.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/hotkey.imageset/hotkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/hotkey.imageset/hotkey.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/shell.imageset/shell@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/shell.imageset/shell@1x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/shell.imageset/shell@1xN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/shell.imageset/shell@1xN.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/hotkey.imageset/hotkey@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/hotkey.imageset/hotkey@1x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Menubar/menuBar.imageset/menuBarIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Menubar/menuBar.imageset/menuBarIcon.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/block.imageset/blacklist@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/block.imageset/blacklist@1x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/history.imageset/history@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Settings/history.imageset/history@1x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Menubar/menuBar.imageset/menuBarIcon@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Menubar/menuBar.imageset/menuBarIcon@1x.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Menubar/menuBarInvert.imageset/menuBarIconInvert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Menubar/menuBarInvert.imageset/menuBarIconInvert.png -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Menubar/menuBarInvert.imageset/menuBarIconInvert@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lihaoyun6/xHistory/HEAD/xHistory/Assets.xcassets/Menubar/menuBarInvert.imageset/menuBarIconInvert@1x.png -------------------------------------------------------------------------------- /xHistory.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /xHistory/xHistory.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Others/textformat.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "textformat.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Colors/buttonBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xEF", 9 | "green" : "0xB6", 10 | "red" : "0x69" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Colors/buttonRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x5E", 9 | "green" : "0x6A", 10 | "red" : "0xEC" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Colors/buttonGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x55", 9 | "green" : "0xC5", 10 | "red" : "0x61" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Colors/buttonYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0x4F", 9 | "green" : "0xBF", 10 | "red" : "0xF4" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/gear.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "gear@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "gear.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/cloud.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "cloud@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "cloud.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/history.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "history@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "history.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/hotkey.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hotkey@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "hotkey.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/block.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "blacklist@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "blacklist.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Menubar/menuBar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "menuBarIcon@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "menuBarIcon.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Menubar/menuBarInvert.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "menuBarIconInvert@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "menuBarIconInvert.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /xHistory/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleURLTypes 6 | 7 | 8 | CFBundleTypeRole 9 | Viewer 10 | CFBundleURLName 11 | com.lihaoyun6.xHistory 12 | CFBundleURLSchemes 13 | 14 | xhistory 15 | 16 | 17 | 18 | SUFeedURL 19 | https://raw.githubusercontent.com/lihaoyun6/xHistory/main/appcast.xml 20 | SUPublicEDKey 21 | dP6zY7RAyK3DtV/dDrQ2V2dB0BY36jADwMKHZIX89u4= 22 | 23 | 24 | -------------------------------------------------------------------------------- /xHistory/Base.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg936\cocoartf2820 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset134 PingFangSC-Semibold;\f1\fnil\fcharset134 PingFangSC-Regular;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0 6 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 7 | 8 | \f0\b\fs24 \cf0 Powerful command line history manager\ 9 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 10 | 11 | \f1\b0\fs10 \cf0 \ 12 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 13 | 14 | \fs20 \cf0 View source code on {\field{\*\fldinst{HYPERLINK "https://github.com/lihaoyun6/xHistory"}}{\fldrslt Github}}} -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Colors/background.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFF", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x1E", 27 | "green" : "0x1E", 28 | "red" : "0x1E" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Colors/background2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0xFF", 9 | "green" : "0xFF", 10 | "red" : "0xFF" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0x53", 27 | "green" : "0x4F", 28 | "red" : "0x4F" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /xHistory/zh-Hans.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg936\cocoartf2820 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset134 PingFangSC-Semibold;\f1\fnil\fcharset134 PingFangSC-Regular;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0 6 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 7 | 8 | \f0\b\fs24 \cf0 \'c7\'bf\'b4\'f3\'b5\'c4\'d6\'d5\'b6\'cb\'c3\'fc\'c1\'ee\'d0\'d0\'c0\'fa\'ca\'b7\'bc\'c7\'c2\'bc\'b9\'dc\'c0\'ed\'c6\'f7\ 9 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 10 | 11 | \f1\b0\fs10 \cf0 \ 12 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 13 | 14 | \fs20 \cf0 \'d4\'b4\'b4\'fa\'c2\'eb\'cd\'d0\'b9\'dc\'d3\'da {\field{\*\fldinst{HYPERLINK "https://github.com/lihaoyun6/xHistory"}}{\fldrslt Github}}} -------------------------------------------------------------------------------- /xHistory/zh-Hant.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg936\cocoartf2821 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset134 PingFangSC-Semibold;\f1\fnil\fcharset134 PingFangSC-Regular;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0 6 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 7 | 8 | \f0\b\fs24 \cf0 \'8f\'8a\'b4\'f3\'b5\'c4\'bd\'4b\'b6\'cb\'c3\'fc\'c1\'ee\'d0\'d0\'9a\'76\'ca\'b7\'d3\'9b\'e4\'9b\'b9\'dc\'c0\'ed\'c6\'f7\ 9 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 10 | 11 | \f1\b0\fs10 \cf0 \ 12 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 13 | 14 | \fs20 \cf0 \'d4\'ad\'ca\'bc\'b4\'61\'d3\'9a\'b9\'dc\'ec\'b6 {\field{\*\fldinst{HYPERLINK "https://github.com/lihaoyun6/xHistory"}}{\fldrslt Github}}} -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Settings/shell.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "shell@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "filename" : "shell@1xN.png", 16 | "idiom" : "universal", 17 | "scale" : "1x" 18 | }, 19 | { 20 | "filename" : "shell.png", 21 | "idiom" : "universal", 22 | "scale" : "2x" 23 | }, 24 | { 25 | "appearances" : [ 26 | { 27 | "appearance" : "luminosity", 28 | "value" : "dark" 29 | } 30 | ], 31 | "filename" : "shellN.png", 32 | "idiom" : "universal", 33 | "scale" : "2x" 34 | }, 35 | { 36 | "idiom" : "universal", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "appearances" : [ 41 | { 42 | "appearance" : "luminosity", 43 | "value" : "dark" 44 | } 45 | ], 46 | "idiom" : "universal", 47 | "scale" : "3x" 48 | } 49 | ], 50 | "info" : { 51 | "author" : "xcode", 52 | "version" : 1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /xh/MakeOverlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MakeOverlay.swift 3 | // xHistory 4 | // 5 | // Created by apple on 2024/11/8. 6 | // 7 | 8 | import AppKit 9 | import CoreGraphics 10 | 11 | func getActiveWindowGeometry() -> (x: Int, y: Int, width: Int, height: Int)? { 12 | guard let appName = NSWorkspace.shared.frontmostApplication?.localizedName else { return nil } 13 | let windowListInfo = CGWindowListCopyWindowInfo([.excludeDesktopElements,.optionOnScreenOnly], kCGNullWindowID) as NSArray? as? [[String: Any]] 14 | if let firstWindow = windowListInfo?.first(where: { $0["kCGWindowOwnerName"] as? String == appName && $0["kCGWindowAlpha"] as? NSNumber != 0 }) { 15 | if let boundsDict = firstWindow["kCGWindowBounds"] as? [String: CGFloat], 16 | let x = boundsDict["X"], let y = boundsDict["Y"], 17 | let width = boundsDict["Width"], let height = boundsDict["Height"] { 18 | return (Int(x), Int(y), Int(width), Int(height)) 19 | } 20 | } 21 | return nil 22 | } 23 | 24 | func openCustomURLWithActiveWindowGeometry(prompt: String = "") { 25 | if let geometry = getActiveWindowGeometry() { 26 | if let url = URL(string: "xhistory://show?x=\(geometry.x)&y=\(geometry.y)&w=\(geometry.width)&h=\(geometry.height)\(prompt)") { 27 | _ = try? NSWorkspace.shared.open(url, options: [.withoutActivation], configuration: [:] ) 28 | } 29 | } 30 | } 31 | 32 | func readPlistValue(filePath: String, key: String) -> Any? { 33 | guard let plist = NSDictionary(contentsOfFile: filePath) else { return nil } 34 | return plist[key] 35 | } 36 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "icon_16x16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "icon_32x32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "icon_32x32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "icon_128x128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "icon_256x256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "icon_512x512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # 2 |

3 | 4 |

xHistory

5 |

一款功能强大的终端历史记录管理工具
[English Version]
[软件主页]

6 |

7 | 8 | ## 运行截图 9 |

10 | 11 | 12 | 13 | xHistory Screenshots 14 | 15 |

16 | 17 | ## 安装与使用 18 | ### 系统版本要求: 19 | - macOS 12.0 及更高版本 20 | 21 | ### 安装: 22 | 可[点此前往](../../releases/latest)下载最新版安装文件. 或使用homebrew安装: 23 | 24 | ```bash 25 | brew install lihaoyun6/tap/xhistory 26 | ``` 27 | 28 | ### 使用: 29 | - xHistory 使用简单, 无需手动配置终端选项即可自动读取多种 shell 的历史记录. 30 | 31 | - 启动后默认显示在菜单栏中(可以隐藏), 也可通过快捷键或命令行来快速打开面板. 32 | - 支持正则搜索, 语法高亮, 自动填充, 智能切片, 命令收藏, 云存档, 黑名单等功能. 33 | 34 | ## 常见问题 35 | **1. 为什么执行命令后没有出现在历史记录面板中?** 36 | > 首次安装并启动 xHistory 后, 您需要登录到新的 shell 会话才行. 37 | 38 | **2. 为什么 xHistory 需要申请辅助功能权限?** 39 | > 因为 xHistory 的 "自动填充" 功能会模拟键盘输入, 必须要有辅助功能权限才能正常工作. 40 | 41 | ## 赞助 42 | 43 | 44 | ## 致谢 45 | [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) @sindresorhus 46 | [SFSMonitor](https://github.com/ClassicalDude/SFSMonitor) @ClassicalDude 47 | [SwiftTreeSitter](https://github.com/ChimeHQ/SwiftTreeSitter) @ChimeHQ 48 | [tree-sitter-bash](https://github.com/tree-sitter/tree-sitter-bash) @tree-sitter 49 | [ChatGPT](https://chat.openai.com) @OpenAI 50 | -------------------------------------------------------------------------------- /xHistory/Supports/CommandLineTool.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandLineTool.swift 3 | // xHistory 4 | // 5 | // Created by apple on 2024/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | class CommandLineTool { 11 | static func runAsRoot(_ command: String, completion: (() -> Void)? = nil) { 12 | let script = "do shell script \"\(command)\" with administrator privileges" 13 | var error: NSDictionary? 14 | 15 | if let scriptObject = NSAppleScript(source: script) { 16 | let _ = scriptObject.executeAndReturnError(&error) 17 | 18 | if error == nil { 19 | completion?() 20 | } else { 21 | print("Error executing command: \(String(describing: error))") 22 | } 23 | } 24 | } 25 | 26 | static func isInstalled() -> Bool { 27 | let attributes = try? fd.attributesOfItem(atPath: "/usr/local/bin/xhistory") 28 | return attributes?[.type] as? FileAttributeType == .typeSymbolicLink 29 | } 30 | 31 | static func install(action: (() -> Void)? = nil) { 32 | if let resourceURL = Bundle.main.resourceURL { 33 | let xhPath = resourceURL.appendingPathComponent("xh").path 34 | if !fd.fileExists(atPath: "/usr/local/bin") { 35 | runAsRoot("/bin/mkdir -p /usr/local/bin;/bin/ln -s '\(xhPath)' /usr/local/bin/xhistory") { 36 | action?() 37 | } 38 | } else { 39 | runAsRoot("/bin/ln -s '\(xhPath)' /usr/local/bin/xhistory") { 40 | action?() 41 | } 42 | } 43 | } 44 | } 45 | 46 | static func uninstall(action: (() -> Void)? = nil) { 47 | runAsRoot("/bin/rm /usr/local/bin/xhistory") { action?() } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## Obj-C/Swift specific 9 | *.hmap 10 | 11 | ## App packaging 12 | *.ipa 13 | *.dSYM.zip 14 | *.dSYM 15 | 16 | ## Playgrounds 17 | timeline.xctimeline 18 | playground.xcworkspace 19 | 20 | # Swift Package Manager 21 | # 22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 23 | # Packages/ 24 | # Package.pins 25 | # Package.resolved 26 | # *.xcodeproj 27 | # 28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 29 | # hence it is not needed unless you have added a package configuration file to your project 30 | # .swiftpm 31 | 32 | .build/ 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | # Pods/ 41 | # 42 | # Add this line if you want to avoid checking in source code from the Xcode workspace 43 | # *.xcworkspace 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | # Carthage/Checkouts 49 | 50 | Carthage/Build/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. 55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 58 | 59 | fastlane/report.xml 60 | fastlane/Preview.html 61 | fastlane/screenshots/**/*.png 62 | fastlane/test_output 63 | -------------------------------------------------------------------------------- /xHistory/Assets.xcassets/Others/textformat.imageset/textformat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /xHistory/Supports/WindowAccessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WindowAccessor.swift 3 | // xHistory 4 | // 5 | // Created by apple on 2024/11/7. 6 | // 7 | 8 | import AppKit 9 | import SwiftUI 10 | 11 | struct WindowAccessor: NSViewRepresentable { 12 | var nsWindow: NSWindow? = nil 13 | var onWindowOpen: ((NSWindow?) -> Void)? 14 | var onWindowActive: ((NSWindow?) -> Void)? 15 | var onWindowClose: (() -> Void)? 16 | 17 | func makeNSView(context: Context) -> NSView { 18 | let view = NSView() 19 | DispatchQueue.main.async { 20 | if let window = view.window { 21 | window.delegate = context.coordinator 22 | context.coordinator.window = window 23 | self.onWindowOpen?(window) 24 | } else { 25 | self.onWindowOpen?(nil) 26 | } 27 | } 28 | return view 29 | } 30 | 31 | func updateNSView(_ nsView: NSView, context: Context) {} 32 | 33 | func makeCoordinator() -> Coordinator { 34 | Coordinator(onWindowOpen: onWindowOpen, onWindowActive: onWindowActive, onWindowClose: onWindowClose) 35 | } 36 | 37 | class Coordinator: NSObject, NSWindowDelegate { 38 | weak var window: NSWindow? // 使用 weak 避免循环引用 39 | var onWindowOpen: ((NSWindow?) -> Void)? 40 | var onWindowActive: ((NSWindow?) -> Void)? 41 | var onWindowClose: (() -> Void)? 42 | 43 | init(onWindowOpen: ((NSWindow?) -> Void)? = nil, 44 | onWindowActive: ((NSWindow?) -> Void)? = nil, 45 | onWindowClose: (() -> Void)? = nil) { 46 | self.onWindowOpen = onWindowOpen 47 | self.onWindowClose = onWindowClose 48 | self.onWindowActive = onWindowActive 49 | } 50 | 51 | func windowWillClose(_ notification: Notification) { 52 | onWindowClose?() 53 | } 54 | 55 | func windowDidBecomeKey(_ notification: Notification) { 56 | onWindowActive?(window) // 将当前窗口传递给 onWindowActive 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 |

3 | 4 |

xHistory

5 |

A powerful command line history manager built with SwiftUI
[中文版本]
[Landing Page]

6 |

7 | 8 | ## Screenshots 9 |

10 | 11 | 12 | 13 | xHistory Screenshots 14 | 15 |

16 | 17 | ## Installation and Usage 18 | ### System Requirements: 19 | - macOS 12.0 and Later 20 | 21 | ### Installation: 22 | Download the latest installation file [here](../../releases/latest) or install via Homebrew: 23 | 24 | ```bash 25 | brew install lihaoyun6/tap/xhistory 26 | ``` 27 | 28 | ### Usage: 29 | - xHistory is simple to use and can automatically read histories from various shells without requiring manual configuration of terminal options. 30 | 31 | - By default, xHistory appears in the menu bar after launching (it can be hidden), and you can quickly open the panel via a shortcut or command line. 32 | - xHistory supports regex searching, syntax highlighting, automatic filling, magic slicing, pin commands, cloud archiving, and more. 33 | 34 | ## Q&A 35 | **1. Why don’t executed commands appear in the history panel?** 36 | > After installing and launching xHistory for the first time, you need to log in to a new shell session for it to work. 37 | 38 | **2. Why does xHistory need Accessibility permissions?** 39 | > The “Auto Fill” feature of xHistory simulates keyboard input, which requires Accessibility permissions to function properly. 40 | 41 | ## Donate 42 | 43 | 44 | ## Thanks 45 | [KeyboardShortcuts](https://github.com/sindresorhus/KeyboardShortcuts) @sindresorhus 46 | [SFSMonitor](https://github.com/ClassicalDude/SFSMonitor) @ClassicalDude 47 | [SwiftTreeSitter](https://github.com/ChimeHQ/SwiftTreeSitter) @ChimeHQ 48 | [tree-sitter-bash](https://github.com/tree-sitter/tree-sitter-bash) @tree-sitter 49 | [ChatGPT](https://chat.openai.com) @OpenAI 50 | -------------------------------------------------------------------------------- /xHistory.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "adb62b15fd29352c871a2d87bb59e504fbe6b96dad95842bceef586d7ab7152c", 3 | "pins" : [ 4 | { 5 | "identity" : "keyboardshortcuts", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/sindresorhus/KeyboardShortcuts", 8 | "state" : { 9 | "revision" : "c3c361f409b8dbe1eab186078b41c330a6a82c9a", 10 | "version" : "2.2.2" 11 | } 12 | }, 13 | { 14 | "identity" : "matrixcolorselector", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/lihaoyun6/MatrixColorSelector.git", 17 | "state" : { 18 | "branch" : "main", 19 | "revision" : "1f68adbfd1c3f876d3cf3ba0600adeb08621cb88" 20 | } 21 | }, 22 | { 23 | "identity" : "sfsmonitor", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/ClassicalDude/SFSMonitor.git", 26 | "state" : { 27 | "branch" : "master", 28 | "revision" : "b221b6a8ea7d521b0b35da60b60c67ac3f6c08cd" 29 | } 30 | }, 31 | { 32 | "identity" : "sparkle", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/sparkle-project/Sparkle", 35 | "state" : { 36 | "revision" : "0ef1ee0220239b3776f433314515fd849025673f", 37 | "version" : "2.6.4" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-argument-parser", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-argument-parser.git", 44 | "state" : { 45 | "revision" : "41982a3656a71c768319979febd796c6fd111d5c", 46 | "version" : "1.5.0" 47 | } 48 | }, 49 | { 50 | "identity" : "swifttreesitter", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", 53 | "state" : { 54 | "revision" : "2599e95310b3159641469d8a21baf2d3d200e61f", 55 | "version" : "0.8.0" 56 | } 57 | }, 58 | { 59 | "identity" : "tree-sitter-bash", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/tree-sitter/tree-sitter-bash.git", 62 | "state" : { 63 | "branch" : "master", 64 | "revision" : "597a5ed6ed4d932fd44697feec988f977081ae59" 65 | } 66 | } 67 | ], 68 | "version" : 3 69 | } 70 | -------------------------------------------------------------------------------- /xHistory/Supports/Sparkle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sparkle.swift 3 | // QuickRecorder 4 | // 5 | // Created by apple on 2024/5/2. 6 | // 7 | 8 | import SwiftUI 9 | import Sparkle 10 | 11 | // This view model class publishes when new updates can be checked by the user 12 | final class CheckForUpdatesViewModel: ObservableObject { 13 | @Published var canCheckForUpdates = false 14 | 15 | init(updater: SPUUpdater) { 16 | updater.publisher(for: \.canCheckForUpdates) 17 | .assign(to: &$canCheckForUpdates) 18 | } 19 | } 20 | 21 | // This is the view for the Check for Updates menu item 22 | // Note this intermediate view is necessary for the disabled state on the menu item to work properly before Monterey. 23 | // See https://stackoverflow.com/questions/68553092/menu-not-updating-swiftui-bug for more info 24 | struct CheckForUpdatesView: View { 25 | @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel 26 | private let updater: SPUUpdater 27 | 28 | init(updater: SPUUpdater) { 29 | self.updater = updater 30 | 31 | // Create our view model for our CheckForUpdatesView 32 | self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater) 33 | } 34 | 35 | var body: some View { 36 | Button("Check for Updates…", action: updater.checkForUpdates) 37 | .disabled(!checkForUpdatesViewModel.canCheckForUpdates) 38 | } 39 | } 40 | 41 | struct UpdaterSettingsView: View { 42 | private let updater: SPUUpdater 43 | 44 | @State private var automaticallyChecksForUpdates: Bool 45 | @State private var automaticallyDownloadsUpdates: Bool 46 | 47 | init(updater: SPUUpdater) { 48 | self.updater = updater 49 | self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates 50 | self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates 51 | } 52 | 53 | var body: some View { 54 | SToggle("Automatically check for updates", isOn: $automaticallyChecksForUpdates) 55 | .onChange(of: automaticallyChecksForUpdates) { newValue in 56 | updater.automaticallyChecksForUpdates = newValue 57 | } 58 | SDivider() 59 | SToggle("Automatically download updates", isOn: $automaticallyDownloadsUpdates) 60 | .disabled(!automaticallyChecksForUpdates) 61 | .onChange(of: automaticallyDownloadsUpdates) { newValue in 62 | updater.automaticallyDownloadsUpdates = newValue 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /xHistory.xcodeproj/xcshareddata/xcschemes/xh.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /xHistory.xcodeproj/xcshareddata/xcschemes/xHistory.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /xh/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // xhistory 4 | // 5 | // Created by apple on 2024/11/6. 6 | // 7 | import ArgumentParser 8 | import Foundation 9 | 10 | struct xhistory: ParsableCommand { 11 | static var configuration = CommandConfiguration(version: "0.1.4") 12 | 13 | @Flag(name: .shortAndLong, help: "Read the history file for the current session") 14 | var session: Bool = false 15 | 16 | @Flag(name: .shortAndLong, help: "Open xHistory overlay and show pinned history") 17 | var pinned: Bool = false 18 | 19 | @Flag(name: .shortAndLong, help: "Open xHistory overlay and show cloud archive") 20 | var archive: Bool = false 21 | 22 | @Option(name: .shortAndLong, help: ArgumentHelp("Get custom shell configuration", valueName: "bash|zsh[23]")) 23 | var config: String? = nil 24 | 25 | @Option(name: .shortAndLong, help: "Read the specified history file") 26 | var file: String? = nil 27 | 28 | mutating func validate() throws { 29 | let arguments = [session, pinned, archive, config != nil, file != nil] 30 | let activeCount = arguments.filter { $0 }.count 31 | if activeCount > 1 { 32 | throw ValidationError("These options cannot be used together!") 33 | } 34 | } 35 | 36 | mutating func run() throws { 37 | if let file = file { 38 | openCustomURLWithActiveWindowGeometry(prompt: "&file=\(file)") 39 | return 40 | } 41 | if session { 42 | if let file = ProcessInfo.processInfo.environment["HISTFILE"] { 43 | openCustomURLWithActiveWindowGeometry(prompt: "&file=\(file)") 44 | } 45 | return 46 | } 47 | if pinned { 48 | openCustomURLWithActiveWindowGeometry(prompt: "&mode=pinned") 49 | return 50 | } 51 | if archive { 52 | openCustomURLWithActiveWindowGeometry(prompt: "&mode=archive") 53 | return 54 | } 55 | if let shell = config { 56 | guard let appSupportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { return } 57 | let plistPath = appSupportDir.appendingPathComponent("xHistory/shellConfig.plist").path 58 | if let config = readPlistValue(filePath: plistPath, key: "customShellConfig") as? Bool, config { 59 | if let value = readPlistValue(filePath: plistPath, key: "historyLimit") { 60 | if shell == "bash" || shell == "zsh" { print("export HISTSIZE=\(value)") } 61 | } 62 | if let value = readPlistValue(filePath: plistPath, key: "realtimeSave") as? Bool, value { 63 | if shell == "bash" { print("export PROMPT_COMMAND=\"history -a\"") } 64 | if shell == "zsh2" { print("setopt INC_APPEND_HISTORY") } 65 | } 66 | if let value = readPlistValue(filePath: plistPath, key: "noDuplicates") as? Bool, value { 67 | if shell == "bash" { print("export HISTCONTROL=ignoredups") } 68 | if shell == "zsh3" { print("setopt HIST_IGNORE_DUPS") } 69 | } 70 | } 71 | return 72 | } 73 | openCustomURLWithActiveWindowGeometry() 74 | } 75 | } 76 | 77 | xhistory.main() 78 | -------------------------------------------------------------------------------- /xHistory/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | xHistory 4 | 5 | Created by apple on 2025/1/7. 6 | 7 | */ 8 | 9 | "OK" = "好"; 10 | "Close" = "關閉"; 11 | "Search" = "搜尋"; 12 | "Don't remind me again" = "不再提醒"; 13 | "You can click on any command or slice\nto fill it into the lower window\n(accessibility permissions required)" = "使用時點選懸浮視窗中的任何命令或切片\n即可將其直接填充到下層視窗中\n(需要授予 xHistory 輔助功能許可權)"; 14 | "If your history has extras (like timestamps)\nyou can preformat it with a custom Regex in\n\"Preferences\" > \"Shell\" > \"Preformatter\"" = "如果你的歷史記錄中含有額外的資訊 (比如時間戳)\n你可以在設定面板中的 \"命令列\" > \"預格式化器\"\n中填寫一條自定義正則表示式以對其進行預匹配"; 15 | "Do you want to install the command line tool?\nAfter installation, you can run \"xhistory\" in yor terminal to quickly open the floating panel.\n\n(You can also install it later in preferences)" = "是否需要安裝 xHistory 命令列工具?\n安裝後在命令列中執行 \"xhistory\"\n即可快速開啟歷史記錄疊加面板.\n\n(也可以稍後在偏好設定中進行安裝)"; 16 | "History" = "歷史記錄"; 17 | "Pinned" = "命令收藏"; 18 | "Magic Slice" = "智能切片"; 19 | "Manual Selection" = "手動選取"; 20 | "Edit Command" = "編輯命令"; 21 | "xHistory Settings" = "xHistory 設置"; 22 | "General" = "一般設置"; 23 | "Launch at Login" = "登錄時啓動"; 24 | "Read History From" = "讀取歷史記錄"; 25 | "Custom" = "自定義"; 26 | "Custom History File Path" = "自定義歷史記錄文件路徑"; 27 | "Zsh-encoded History" = "Zsh 編碼格式"; 28 | "Show Menu bar Icon" = "顯示菜單欄圖標"; 29 | "Menu Bar Icon" = "菜單欄圖標樣式"; 30 | "Show Dock Icon" = "在 Dock 上顯示"; 31 | "History Panel Opacity" = "懸浮窗不透明度"; 32 | "History" = "歷史記錄"; 33 | "Merge adjacent duplicates" = "合併相鄰的重複記錄"; 34 | "Close history panel after filling" = "自動填寫後關閉歷史面板"; 35 | "Auto-press Return after filling" = "自動填寫後幫我按下回車鍵"; 36 | "Add trailing space when filling" = "自動填寫時在末尾添加空格"; 37 | "Highlight" = "高亮設置"; 38 | "Hotkey" = "快捷鍵"; 39 | "Open History Panel" = "打開歷史記錄懸浮窗"; 40 | "Open panel and show pinned history" = "打開懸浮窗並顯示命令收藏"; 41 | "Open History Panel as Overlay" = "以疊加層模式打開懸浮窗"; 42 | "Open overlay and show pinned history" = "打開疊加層並顯示命令收藏"; 43 | "xHistory will detect the current frontmost window and open a floating panel of the same size on top of it." = "xHistory 會自動檢測當前的最前置窗口, 並在它之上打開一個與它尺寸相同的懸浮窗."; 44 | "Swap action button positions" = "交換操作按鈕的位置"; 45 | "Put \"Copy\", \"Pin\" and \"Expand\" buttons on the other side of history items.\nThis is useful for full screen or very long window." = "將\"複製\",\"固定\"和\"展開\"三個按鈕顯示在歷史記錄的另一側.\n當全屏使用或窗口很長的時候, 這會有助於減少鼠標移動距離."; 46 | "Others" = "其他設置"; 47 | "Preformatter" = "預格式化器"; 48 | "Enter regular expression here" = "在此輸入正則表達式"; 49 | "You can use regular expressions to match each line of history.\nxHistory will only show you the content in the matching groups." = "你可以使用一段正則表達式對每一條歷史記錄進行預匹配,\nxHistory 只會向你展示符合條件的匹配組中的內容."; 50 | "Command Line Tool" = "工具"; 51 | "Install" = "安裝"; 52 | "Uninstall" = "解除安裝"; 53 | "After installation, you can run \"xhistory\" in yor terminal to quickly open the floating panel." = "安裝此工具後, 在中執行 \"xhistory\" 即可快速打開疊加面板."; 54 | "Syntax Highlighting" = "語法高亮顯示"; 55 | "Highlight Color Scheme" = "高亮配色方案"; 56 | "Save" = "保存"; 57 | "Hover over a color to see more information and click to modify it." = "將鼠標懸停在任意顏色上以查看更多信息, 點擊即可修改配色."; 58 | "Show Colors..." = "顯示顏色…"; 59 | "The color of functions in the code" = "代碼中命令/函數的顏色"; 60 | "The color of keywords in the code" = "代碼中內置關鍵字的顏色"; 61 | "The color of strings in the code" = "代碼中字符串的顏色"; 62 | "The color of properties in the code" = "代碼中變量名稱的顏色"; 63 | "The color of operators in the code" = "代碼中操作符的顏色"; 64 | "The color of constants in the code" = "代碼中選項/參數的顏色"; 65 | "The color of numbers in the code" = "代碼中數字值的顏色"; 66 | "The color of embedded code in the code" = "代碼中嵌入命令塊的顏色"; 67 | "Reset Color Scheme" = "重設配色方案"; 68 | "Update" = "更新設置"; 69 | "Automatically check for updates" = "自動檢查程序更新"; 70 | "Automatically download updates" = "自動下載程序更新"; 71 | "Check for Updates…" = "檢查程序更新…"; 72 | "Shell" = ""; 73 | "Shell Configuration" = "設置"; 74 | "Custom Configuration (for Bash & Zsh)"= "啓用自定義配置 (針對 Bash 和 Zsh)"; 75 | "Real-time History Saving" = "實時保存歷史記錄"; 76 | "Ignore Consecutive Duplicates" = "忽略連續的重複記錄"; 77 | "Maximum Number of History Items" = "歷史記錄保存數量"; 78 | "Quit" = "退出"; 79 | "These settings will only take effect in newly logged-in shells when you modify them." = "當你修改這些配置後, 需要新登錄的 Shell 纔會生效."; 80 | "Blacklist" = "屏蔽列表"; 81 | "The following commands will be ignored from the history" = "下列命令將會從歷史記錄中被忽略"; 82 | "Enter Command" = "輸入命令"; 83 | "Add to List" = "添加到列表"; 84 | "Cancel" = "取消"; 85 | "Regular expression" = "正則表達式"; 86 | "Case sensitive" = "區分大小寫"; 87 | "Cloud" = "雲存檔"; 88 | "Archive" = "雲端存檔"; 89 | "Cloud Archiving" = "啓用雲端存檔"; 90 | "Archive Folder" = "存檔文件夾"; 91 | "Select a folder in iCloud Drive to store and sync history across multiple devices." = "選擇一個 iCloud 雲盤內的文件夾, 用於在多臺設備之間存儲並共享歷史記錄"; 92 | "Select..." = "選擇..."; 93 | "Archives" = "存檔列表"; 94 | "Select an archive" = "請選擇一個存檔"; 95 | "Refresh" = "刷新"; 96 | "Not selected" = "未選擇"; 97 | "Delete" = "刪除"; 98 | "Confirm" = "確認"; 99 | "Are you sure?" = "確認進行此操作嗎?"; 100 | "You will not be able to recover it!" = "此操作無法復原, 請謹慎確認!"; 101 | "Delete This Archive?" = "確認要刪除此存檔嗎?"; 102 | "You can add a \"#\" at the beginning of a keyword to convert it to a regex pattern" = "若要使用正則表示式進行匹配, 請在遮蔽詞條前寫上 # 符號"; 103 | "Switch to \"History\" page" = "切換至\"歷史記錄\"頁面"; 104 | "Switch to \"Pinned\" page" = "切換至\"命令收藏\"頁面"; 105 | "Switch to \"Archive\" page" = "切換至\"雲端存檔\"頁面"; 106 | -------------------------------------------------------------------------------- /xHistory/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | xHistory 4 | 5 | Created by apple on 2024/11/8. 6 | 7 | */ 8 | 9 | "OK" = "好"; 10 | "Close" = "关闭"; 11 | "Search" = "搜索"; 12 | "xHistory Tips" = "xHistory 小贴士"; 13 | "Don't remind me again" = "不再提醒"; 14 | "You can click on any command or slice\nto fill it into the lower window\n(accessibility permissions required)" = "使用时点击悬浮窗口中的任何命令或切片\n即可将其直接填充到下层窗口中\n(需要授予 xHistory 辅助功能权限)"; 15 | "If your history has extras (like timestamps)\nyou can preformat it with a custom Regex in\n\"Preferences\" > \"Shell\" > \"Preformatter\"" = "如果你的历史记录中含有额外的信息 (比如时间戳)\n你可以在设置面板中的 \"命令行\" > \"预格式化器\"\n中填写一条自定义正则表达式以对其进行预匹配"; 16 | "Do you want to install the command line tool?\nAfter installation, you can run \"xhistory\" in yor terminal to quickly open the floating panel.\n\n(You can also install it later in preferences)" = "是否需要安装 xHistory 命令行工具?\n安装后在命令行中执行 \"xhistory\"\n即可快速打开历史记录叠加面板.\n\n(也可以稍后在偏好设置中进行安装)"; 17 | "History" = "历史记录"; 18 | "Pinned" = "命令收藏"; 19 | "Magic Slice" = "智能切片"; 20 | "Manual Selection" = "手动选取"; 21 | "Edit Command" = "编辑命令"; 22 | "xHistory Settings" = "xHistory 设置"; 23 | "General" = "一般设置"; 24 | "Launch at Login" = "登录时启动"; 25 | "Read History From" = "读取历史记录"; 26 | "Custom" = "自定义"; 27 | "Custom History File Path" = "自定义历史记录文件路径"; 28 | "Zsh-encoded History" = "Zsh 编码格式"; 29 | "Show Menu bar Icon" = "显示菜单栏图标"; 30 | "Menu Bar Icon" = "菜单栏图标样式"; 31 | "Show Dock Icon" = "显示在 Dock 栏上"; 32 | "History Panel Opacity" = "悬浮窗不透明度"; 33 | "History" = "历史记录"; 34 | "Merge adjacent duplicates" = "合并相邻的重复记录"; 35 | "Close history panel after filling" = "自动填写后关闭历史面板"; 36 | "Auto-press Return after filling" = "自动填写后帮我按下回车键"; 37 | "Add trailing space when filling" = "自动填写时在末尾添加空格"; 38 | "Highlight" = "高亮设置"; 39 | "Hotkey" = "快捷键"; 40 | "Open History Panel" = "打开历史记录悬浮窗"; 41 | "Open panel and show pinned history" = "打开悬浮窗并显示命令收藏"; 42 | "Open History Panel as Overlay" = "以叠加层模式打开悬浮窗"; 43 | "Open overlay and show pinned history" = "打开叠加层并显示命令收藏"; 44 | "xHistory will detect the current frontmost window and open a floating panel of the same size on top of it." = "xHistory 会自动检测当前的最前置窗口, 并在它之上打开一个与它尺寸相同的悬浮窗."; 45 | "Swap action button positions" = "交换操作按钮的位置"; 46 | "Put \"Copy\", \"Pin\" and \"Expand\" buttons on the other side of history items.\nThis is useful for full screen or very long window." = "将\"复制\",\"固定\"和\"展开\"三个按钮显示在历史记录的另一侧.\n当全屏使用或窗口很长的时候, 这会有助于减少鼠标移动距离."; 47 | "Others" = "其他设置"; 48 | "Preformatter" = "预格式化器"; 49 | "Enter regular expression here" = "在此输入正则表达式"; 50 | "You can use regular expressions to match each line of history.\nxHistory will only show you the content in the matching groups." = "你可以使用一段正则表达式对每一条历史记录进行预匹配,\nxHistory 只会向你展示符合条件的匹配组中的内容."; 51 | "Command Line Tool" = "命令行工具"; 52 | "Install" = "安装"; 53 | "Uninstall" = "卸载"; 54 | "After installation, you can run \"xhistory\" in yor terminal to quickly open the floating panel." = "安装此工具后, 在命令行中执行 \"xhistory\" 即可快速打开叠加面板."; 55 | "Syntax Highlighting" = "语法高亮显示"; 56 | "Highlight Color Scheme" = "高亮配色方案"; 57 | "Save" = "保存"; 58 | "Hover over a color to see more information and click to modify it." = "将鼠标悬停在任意颜色上以查看更多信息, 点击即可修改配色."; 59 | "Show Colors..." = "显示颜色…"; 60 | "The color of functions in the code" = "代码中命令/函数的颜色"; 61 | "The color of keywords in the code" = "代码中内置关键字的颜色"; 62 | "The color of strings in the code" = "代码中字符串的颜色"; 63 | "The color of properties in the code" = "代码中变量名称的颜色"; 64 | "The color of operators in the code" = "代码中操作符的颜色"; 65 | "The color of constants in the code" = "代码中选项/参数的颜色"; 66 | "The color of numbers in the code" = "代码中数字值的颜色"; 67 | "The color of embedded code in the code" = "代码中嵌入命令块的颜色"; 68 | "Reset Color Scheme" = "重设配色方案"; 69 | "Update" = "更新设置"; 70 | "Automatically check for updates" = "自动检查程序更新"; 71 | "Automatically download updates" = "自动下载程序更新"; 72 | "Check for Updates…" = "检查程序更新…"; 73 | "Shell" = "命令行"; 74 | "Shell Configuration" = "命令行设置"; 75 | "Custom Configuration (for Bash & Zsh)"= "启用自定义配置 (针对 Bash 和 Zsh)"; 76 | "Real-time History Saving" = "实时保存历史记录"; 77 | "Ignore Consecutive Duplicates" = "忽略连续的重复记录"; 78 | "Maximum Number of History Items" = "历史记录保存数量"; 79 | "Quit" = "退出"; 80 | "These settings will only take effect in newly logged-in shells when you modify them." = "当你修改这些配置后, 需要新登录的 Shell 才会生效."; 81 | "Blacklist" = "屏蔽列表"; 82 | "The following commands will be ignored from the history" = "下列命令将会从历史记录中被忽略"; 83 | "Enter Command" = "输入命令"; 84 | "Add to List" = "添加到列表"; 85 | "Cancel" = "取消"; 86 | "Regular expression" = "正则表达式"; 87 | "Case sensitive" = "区分大小写"; 88 | "Cloud" = "云存档"; 89 | "Archive" = "云端存档"; 90 | "Cloud Archiving" = "启用云端存档"; 91 | "Archive Folder" = "存档文件夹"; 92 | "Select a folder in iCloud Drive to store and sync history across multiple devices." = "选择一个 iCloud 云盘内的文件夹, 用于在多台设备之间存储并共享历史记录"; 93 | "Select..." = "选择..."; 94 | "Archives" = "存档列表"; 95 | "Select an archive" = "请选择一个存档"; 96 | "Refresh" = "刷新"; 97 | "Not selected" = "未选择"; 98 | "Delete" = "删除"; 99 | "Confirm" = "确认"; 100 | "Are you sure?" = "确认进行此操作吗?"; 101 | "You will not be able to recover it!" = "此操作无法复原, 请谨慎确认!"; 102 | "Delete This Archive?" = "确认要删除此存档吗?"; 103 | "You can add a \"#\" at the beginning of a keyword to convert it to a regex pattern" = "若要使用正则表达式进行匹配, 请在屏蔽词条前添加 # 符号"; 104 | "Switch to \"History\" page" = "切换至\"历史记录\"页面"; 105 | "Switch to \"Pinned\" page" = "切换至\"命令收藏\"页面"; 106 | "Switch to \"Archive\" page" = "切换至\"云端存档\"页面"; 107 | -------------------------------------------------------------------------------- /xHistory/Supports/GroupForm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SInfoButton.swift 3 | // AirBattery 4 | // 5 | // Created by apple on 2024/10/28. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HoverButton: View { 11 | var color: Color = .primary 12 | var secondaryColor: Color = .blue 13 | var action: () -> Void 14 | @ViewBuilder let label: () -> Content 15 | @State private var isHovered: Bool = false 16 | 17 | var body: some View { 18 | Button(action: { 19 | action() 20 | }, label: { 21 | label().foregroundStyle(isHovered ? secondaryColor : color) 22 | }) 23 | .buttonStyle(.plain) 24 | .onHover(perform: { isHovered = $0 }) 25 | } 26 | } 27 | 28 | struct SForm: View { 29 | var spacing: CGFloat = 30 30 | var noSpacer: Bool = false 31 | @ViewBuilder let content: () -> Content 32 | 33 | var body: some View { 34 | VStack(spacing: spacing) { 35 | content() 36 | if !noSpacer { 37 | Spacer().frame(minHeight: 0) 38 | } 39 | } 40 | .padding(.bottom, noSpacer ? 0 : -spacing) 41 | .padding() 42 | .frame(maxWidth: .infinity) 43 | 44 | } 45 | } 46 | 47 | struct SGroupBox: View { 48 | var label: LocalizedStringKey? = nil 49 | @ViewBuilder let content: () -> Content 50 | 51 | var body: some View { 52 | GroupBox(label: label != nil ? Text(label!).font(.headline) : nil) { 53 | VStack(spacing: 10) { content() }.padding(5) 54 | } 55 | } 56 | } 57 | 58 | struct SItem: View { 59 | var label: LocalizedStringKey? = nil 60 | var spacing: CGFloat = 8 61 | @ViewBuilder let content: () -> Content 62 | 63 | var body: some View { 64 | HStack(spacing: spacing) { 65 | if let label = label { Text(label) } 66 | Spacer() 67 | content() 68 | }.frame(height: 16) 69 | } 70 | } 71 | 72 | struct SDivider: View { 73 | var body: some View { 74 | Divider().opacity(0.5) 75 | } 76 | } 77 | 78 | struct SSlider: View { 79 | var label: LocalizedStringKey? = nil 80 | @Binding var value: Int 81 | var range: ClosedRange = 0...100 82 | var width: CGFloat = .infinity 83 | 84 | var body: some View { 85 | HStack { 86 | if let label = label { 87 | Text(label) 88 | } 89 | Spacer() 90 | Slider(value: 91 | Binding(get: { Double(value) }, 92 | set: { newValue in 93 | let base: Int = Int(newValue.rounded()) 94 | let modulo: Int = base % 1 95 | value = base - modulo 96 | }), in: range).frame(maxWidth: width) 97 | }.frame(height: 16) 98 | } 99 | } 100 | 101 | struct SInfoButton: View { 102 | var tips: LocalizedStringKey 103 | @State private var isPresented: Bool = false 104 | 105 | var body: some View { 106 | Button(action: { 107 | isPresented = true 108 | }, label: { 109 | Image(systemName: "info.circle") 110 | .font(.system(size: 15, weight: .light)) 111 | .opacity(0.5) 112 | }) 113 | .buttonStyle(.plain) 114 | .sheet(isPresented: $isPresented) { 115 | VStack(alignment: .trailing) { 116 | GroupBox { Text(tips).padding() } 117 | Button(action: { 118 | isPresented = false 119 | }, label: { 120 | Text("OK").frame(width: 30) 121 | }).keyboardShortcut(.defaultAction) 122 | }.padding() 123 | } 124 | } 125 | } 126 | 127 | struct SButton: View { 128 | var title: LocalizedStringKey 129 | var buttonTitle: LocalizedStringKey 130 | var tips: LocalizedStringKey? 131 | var action: () -> Void 132 | 133 | init(_ title: LocalizedStringKey, buttonTitle: LocalizedStringKey, tips: LocalizedStringKey? = nil, action: @escaping () -> Void) { 134 | self.title = title 135 | self.buttonTitle = buttonTitle 136 | self.tips = tips 137 | self.action = action 138 | } 139 | 140 | var body: some View { 141 | HStack(spacing: 4) { 142 | Text(title) 143 | Spacer() 144 | if let tips = tips { SInfoButton(tips: tips) } 145 | Button(buttonTitle, 146 | action: { action() }) 147 | }.frame(height: 16) 148 | } 149 | } 150 | 151 | struct SField: View { 152 | var title: LocalizedStringKey 153 | var placeholder: LocalizedStringKey 154 | var tips: LocalizedStringKey? 155 | @Binding var text: String 156 | var width: Double 157 | 158 | init(_ title: LocalizedStringKey, placeholder:LocalizedStringKey = "", tips: LocalizedStringKey? = nil, text: Binding, width: Double = .infinity) { 159 | self.title = title 160 | self.placeholder = placeholder 161 | self.tips = tips 162 | self._text = text 163 | self.width = width 164 | } 165 | 166 | var body: some View { 167 | HStack(spacing: 4) { 168 | Text(title) 169 | Spacer() 170 | if let tips = tips { SInfoButton(tips: tips) } 171 | TextField(placeholder, text: $text) 172 | .textFieldStyle(.roundedBorder) 173 | .multilineTextAlignment(.trailing) 174 | .frame(maxWidth: width) 175 | } 176 | } 177 | } 178 | 179 | struct SPicker: View { 180 | var title: LocalizedStringKey 181 | @Binding var selection: T 182 | var style: Style 183 | var tips: LocalizedStringKey? 184 | @ViewBuilder let content: () -> Content 185 | 186 | init(_ title: LocalizedStringKey, selection: Binding, style: Style = .menu, tips: LocalizedStringKey? = nil, @ViewBuilder content: @escaping () -> Content) { 187 | self.title = title 188 | self._selection = selection 189 | self.style = style 190 | self.tips = tips 191 | self.content = content 192 | } 193 | 194 | var body: some View { 195 | HStack { 196 | Text(title) 197 | Spacer() 198 | if let tips = tips { SInfoButton(tips: tips) } 199 | Picker(selection: $selection, content: { content() }, label: {}) 200 | .fixedSize() 201 | .pickerStyle(style) 202 | .buttonStyle(.borderless) 203 | }.frame(height: 16) 204 | } 205 | } 206 | 207 | struct SToggle: View { 208 | var title: LocalizedStringKey 209 | @Binding var isOn: Bool 210 | var tips: LocalizedStringKey? 211 | 212 | init(_ title: LocalizedStringKey, isOn: Binding, tips: LocalizedStringKey? = nil) { 213 | self.title = title 214 | self._isOn = isOn 215 | self.tips = tips 216 | } 217 | 218 | var body: some View { 219 | HStack(spacing: 4) { 220 | Text(title) 221 | Spacer() 222 | if let tips = tips { SInfoButton(tips: tips) } 223 | Toggle("", isOn: $isOn) 224 | .toggleStyle(.switch) 225 | .scaleEffect(0.7) 226 | .frame(width: 32) 227 | }.frame(height: 16) 228 | } 229 | } 230 | 231 | struct SSteper: View { 232 | var title: LocalizedStringKey 233 | @Binding var value: Int 234 | var min: Int 235 | var max: Int 236 | var width: CGFloat 237 | var tips: LocalizedStringKey? 238 | 239 | init(_ title: LocalizedStringKey, value: Binding, min: Int = 0, max: Int = 100, width: CGFloat = 45, tips: LocalizedStringKey? = nil) { 240 | self.title = title 241 | self._value = value 242 | self.tips = tips 243 | self.width = width 244 | self.min = min 245 | self.max = max 246 | } 247 | 248 | var body: some View { 249 | HStack(spacing: 0) { 250 | Text(title) 251 | Spacer() 252 | if let tips = tips { SInfoButton(tips: tips) } 253 | TextField("", value: $value, formatter: NumberFormatter()) 254 | .textFieldStyle(.roundedBorder) 255 | .multilineTextAlignment(.trailing) 256 | .frame(width: width) 257 | .onChange(of: value) { newValue in 258 | if newValue > max { value = max } 259 | if newValue < min { value = min } 260 | } 261 | Stepper("", value: $value) 262 | .padding(.leading, -6) 263 | }.frame(height: 16) 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /xHistory/Supports/SyntaxHighlighter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyntaxHighlighter.swift 3 | // xHistory 4 | // 5 | // Created by apple on 2024/11/6. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | import SwiftTreeSitter 11 | import TreeSitterBash 12 | 13 | /*struct SyntaxHighlighter { 14 | let commandStyle = AttributeContainer([.foregroundColor: NSColor.systemOrange]) 15 | let parameterStyle = AttributeContainer([.foregroundColor: NSColor.systemGreen]) 16 | let optionStyle = AttributeContainer([.foregroundColor: NSColor.systemMint]) 17 | let symbolStyle = AttributeContainer([.foregroundColor: NSColor.systemGray]) 18 | let stringStyle = AttributeContainer([.foregroundColor: NSColor.textColor]) 19 | 20 | func highlightBashSyntax(in command: String) -> AttributedString { 21 | var attributedString = AttributedString(command) 22 | 23 | let patterns: [(String, AttributeContainer)] = [ 24 | // 匹配闭合的引号内的内容(包括引号本身) 25 | (#"(['\"`]).*?\1"#, parameterStyle), 26 | // 匹配操作符 | 或 ; 或 & 27 | (#"[\|;&]\s*"#, symbolStyle), 28 | // 匹配行首的命令和 | 或 ; 后的命令 29 | (#"(?<=^|[;|]\x{200B}\s?)(\S+)"#, commandStyle), 30 | // 匹配以 - 或 -- 开头的选项 31 | (#"(?<=\s\x{200B})-\S+"#, optionStyle), 32 | // 匹配不被空格分隔的路径或 URL 33 | //(#"(?<=\s|^)([^\s'\"|;]+(?:\\\s[^\s'\"|;]+)*)"#, stringStyle) 34 | ] 35 | 36 | for (pattern, style) in patterns { 37 | if let regex = try? NSRegularExpression(pattern: pattern) { 38 | let nsRange = NSRange(command.startIndex..() 56 | @AppStorage("highlighting") var highlighting = true 57 | 58 | func getHighlightedText(for source: String) -> NSAttributedString { 59 | if let cachedResult = cache.object(forKey: source as NSString) { 60 | return cachedResult 61 | } else { 62 | let highlightedText = bashHighlighter(source) 63 | cache.setObject(highlightedText, forKey: source as NSString) 64 | return highlightedText 65 | } 66 | } 67 | 68 | func clearCache() { cache.removeAllObjects() } 69 | 70 | func getHighlightedTextAsync(for source: String, completion: @escaping (NSAttributedString) -> Void) { 71 | if let cachedResult = cache.object(forKey: source as NSString) { 72 | completion(cachedResult) 73 | } else { 74 | DispatchQueue.global(qos: .userInitiated).async { 75 | let highlightedText = self.bashHighlighter(source) 76 | self.cache.setObject(highlightedText, forKey: source as NSString) 77 | DispatchQueue.main.async { completion(highlightedText) } 78 | } 79 | } 80 | } 81 | 82 | func bashHighlighter(_ source: String) -> NSAttributedString { 83 | let attributedString = NSMutableAttributedString(string: source) 84 | if !highlighting { return attributedString } 85 | do { 86 | let bashConfig = try LanguageConfiguration(tree_sitter_bash(), name: "Bash") 87 | let parser = Parser() 88 | try parser.setLanguage(bashConfig.language) 89 | let tree = parser.parse(source)! 90 | let query = bashConfig.queries[.highlights]! 91 | let cursor = query.execute(in: tree) 92 | let highlights = cursor 93 | .resolve(with: .init(string: source)) 94 | .highlights() 95 | 96 | let colorMap: [String: NSColor] = [ 97 | "function": ud.nsColor(forKey: "functionColor") ?? .systemOrange, 98 | "keyword": ud.nsColor(forKey: "keywordColor") ?? .systemPink, 99 | "string": ud.nsColor(forKey: "stringColor") ?? .systemGreen, 100 | "comment": .gray, 101 | "property": ud.nsColor(forKey: "propertyColor") ?? .systemBlue, 102 | "operator": ud.nsColor(forKey: "operatorColor") ?? .systemGray, 103 | "constant": ud.nsColor(forKey: "constantColor") ?? .systemMint, 104 | "number": ud.nsColor(forKey: "numberColor") ?? .red, 105 | "embedded": ud.nsColor(forKey: "embeddedColor") ?? .systemPurple 106 | ] 107 | 108 | for highlight in highlights { 109 | let type = highlight.name 110 | let color = colorMap[type] ?? .labelColor 111 | let range = NSRange(location: highlight.range.location, length: highlight.range.length) 112 | attributedString.addAttribute(.foregroundColor, value: color, range: range) 113 | } 114 | } catch { 115 | print("Error parsing source code: \(error)") 116 | } 117 | 118 | return attributedString 119 | } 120 | 121 | /*func bashHighlighterList(_ source: String) -> [AttributedString] { 122 | let attributedString = NSMutableAttributedString(string: source) 123 | var result: [AttributedString] = [] 124 | var currentSubstring = NSMutableAttributedString() 125 | var currentColor: NSColor? = nil 126 | 127 | do { 128 | let bashConfig = try LanguageConfiguration(tree_sitter_bash(), name: "Bash") 129 | let parser = Parser() 130 | try parser.setLanguage(bashConfig.language) 131 | let tree = parser.parse(source)! 132 | let query = bashConfig.queries[.highlights]! 133 | let cursor = query.execute(in: tree) 134 | let highlights = cursor 135 | .resolve(with: .init(string: source)) 136 | .highlights() 137 | 138 | let colorMap: [String: NSColor] = [ 139 | "function": .systemOrange, 140 | "keyword": .systemPink, 141 | "string": .systemGreen, 142 | "comment": .systemGray, 143 | "property": .systemBlue, 144 | "constant": .systemMint, 145 | "number": .systemRed, 146 | "embedded": .systemPurple 147 | ] 148 | 149 | var lastIndex = 0 150 | 151 | for highlight in highlights { 152 | let type = highlight.name 153 | //if type == "operator" { continue } 154 | 155 | let color = colorMap[type] ?? .labelColor 156 | let range = NSRange(location: highlight.range.location, length: highlight.range.length) 157 | 158 | // 添加未标记的普通字符串部分 159 | if range.location > lastIndex { 160 | let unhighlightedRange = NSRange(location: lastIndex, length: range.location - lastIndex) 161 | let unhighlightedText = attributedString.attributedSubstring(from: unhighlightedRange) 162 | appendUnhighlightedText(unhighlightedText, to: &result) 163 | } 164 | 165 | // 获取高亮内容 166 | let highlightText = attributedString.attributedSubstring(from: range) 167 | if color != currentColor { 168 | if currentSubstring.length > 0 { 169 | result.append(AttributedString(currentSubstring)) 170 | } 171 | currentSubstring = NSMutableAttributedString(attributedString: highlightText) 172 | currentSubstring.addAttribute(.foregroundColor, value: color, range: NSRange(location: 0, length: currentSubstring.length)) 173 | currentColor = color 174 | } else { 175 | currentSubstring.append(highlightText) 176 | } 177 | 178 | lastIndex = range.location + range.length 179 | } 180 | 181 | if currentSubstring.length > 0 { 182 | result.append(AttributedString(currentSubstring)) 183 | } 184 | 185 | // 添加剩余的未标记普通字符串部分 186 | if lastIndex < attributedString.length { 187 | let remainingRange = NSRange(location: lastIndex, length: attributedString.length - lastIndex) 188 | let remainingText = attributedString.attributedSubstring(from: remainingRange) 189 | appendUnhighlightedText(remainingText, to: &result) 190 | } 191 | 192 | } catch { 193 | print("Error parsing source code: \(error)") 194 | } 195 | 196 | return result.filter { !$0.characters.isEmpty && String($0.characters) != " " } 197 | } 198 | 199 | // 分离未标记的普通字符串,处理空格分隔 200 | private func appendUnhighlightedText(_ unhighlightedText: NSAttributedString, to result: inout [AttributedString]) { 201 | let plainText = unhighlightedText.string 202 | var lastStartIndex = plainText.startIndex 203 | var currentIndex = plainText.startIndex 204 | 205 | while currentIndex < plainText.endIndex { 206 | if plainText[currentIndex] == " " { 207 | // 检查是否为反斜线转义的空格 208 | if currentIndex > plainText.startIndex, plainText[plainText.index(before: currentIndex)] != "\\" { 209 | let substring = String(plainText[lastStartIndex.. 0.1{ 34 | self.lastUpdate = Date().timeIntervalSince1970 35 | self.updateHistory() 36 | if self.cloudSync && self.cloudDirectory != "" { self.needArchive = true } 37 | } 38 | } 39 | } 40 | 41 | func readHistory(file: String? = nil) -> [String] { 42 | @AppStorage("historyFile") var historyFile = "~/.bash_history" 43 | @AppStorage("noSameLine") var noSameLine = true 44 | @AppStorage("preFormatter") var preFormatter = "" 45 | let blockedItems = (ud.object(forKey: "blockedCommands") as? [String]) ?? [] 46 | 47 | var fileURL = historyFile.absolutePath.url 48 | if let file = file { fileURL = file.absolutePath.url } 49 | var lines = fileURL.readHistory?.components(separatedBy: .newlines).map({ $0.trimmingCharacters(in: .whitespaces) }) ?? [] 50 | lines = lines.filter({ !$0.isEmpty }) 51 | if preFormatter != "" { lines = lines.format(usingRegex: preFormatter) } 52 | let normalBlock = blockedItems.filter({ !$0.startsWith(character: "#") }) 53 | let regexBlock = blockedItems.filter({ $0.startsWith(character: "#") }).map({ String($0.dropFirst()) }) 54 | lines.removeAll(where: { normalBlock.contains($0) }) 55 | lines = filterNonMatchingStrings(regexList: regexBlock, stringList: lines) 56 | if noSameLine { return lines.removingAdjacentDuplicates() } 57 | return lines 58 | } 59 | 60 | func updateHistory(file: String? = nil) { 61 | DispatchQueue.main.async { 62 | if NSApp.windows.first(where: { $0.title == "xHistory Panel".local && $0.isVisible }) != nil || menuPopover.isShown { 63 | self.historys = self.readHistory(file: file).reversed() 64 | } 65 | } 66 | } 67 | 68 | func reHighlight() { 69 | SyntaxHighlighter.shared.clearCache() 70 | HistoryCopyer.shared.historys.removeAll() 71 | HistoryCopyer.shared.updateHistory() 72 | for cmd in HistoryCopyer.shared.readHistory() { 73 | SyntaxHighlighter.shared.getHighlightedTextAsync(for: cmd) { _ in } 74 | } 75 | } 76 | 77 | func filterNonMatchingStrings(regexList: [String], stringList: [String]) -> [String] { 78 | let regexPatterns = regexList.compactMap { pattern in 79 | try? NSRegularExpression(pattern: pattern) 80 | } 81 | let nonMatchingStrings = stringList.filter { string in 82 | for regex in regexPatterns { 83 | let range = NSRange(location: 0, length: string.utf16.count) 84 | if regex.firstMatch(in: string, options: [], range: range) != nil { 85 | return false 86 | } 87 | } 88 | return true 89 | } 90 | return nonMatchingStrings 91 | } 92 | } 93 | 94 | extension Array where Element: Equatable { 95 | func removingAdjacentDuplicates() -> [Element] { 96 | reduce(into: []) { result, element in 97 | if result.last != element { 98 | result.append(element) 99 | } 100 | } 101 | } 102 | } 103 | 104 | extension Array where Element == String { 105 | func format(usingRegex regexPattern: String) -> [String] { 106 | do { 107 | let regex = try NSRegularExpression(pattern: regexPattern) 108 | return self.compactMap { element in 109 | // 转换为 NSRange 110 | let range = NSRange(element.startIndex.. 1, // 确保捕获组范围存在 114 | let commandRange = Range(match.range(at: 1), in: element) { 115 | return String(element[commandRange]) 116 | } 117 | 118 | return nil 119 | } 120 | } catch { 121 | //print("Invalid regular expression: \(error)") 122 | return [] 123 | } 124 | } 125 | } 126 | 127 | extension URL { 128 | var lastLine: String? { 129 | do { 130 | return try self.readLastLine 131 | } catch { 132 | return self.readLastLineZ 133 | } 134 | } 135 | 136 | var isFileUTF8Encoded: Bool { 137 | do { 138 | _ = try String(contentsOf: self, encoding: .utf8) 139 | return true 140 | } catch { 141 | return false 142 | } 143 | } 144 | 145 | var readHistory: String? { 146 | do { 147 | return try String(contentsOf: self, encoding: .utf8) 148 | } catch { 149 | return self.readZshHistory 150 | } 151 | } 152 | 153 | private var readZshHistory: String? { 154 | var zshHistoryContent = "" 155 | func unmetafy(_ bytes: inout [UInt8]) -> String { 156 | var index = 0 157 | let zshMeta: UInt8 = 0x83 158 | 159 | while index < bytes.count { 160 | if bytes[index] == zshMeta { 161 | bytes.remove(at: index) 162 | if index < bytes.count { bytes[index] ^= 32 } 163 | } else { 164 | index += 1 165 | } 166 | } 167 | return String(decoding: bytes, as: UTF8.self) 168 | } 169 | 170 | if let fileHandle = FileHandle(forReadingAtPath: self.path) { 171 | defer { fileHandle.closeFile() } 172 | let data = fileHandle.readDataToEndOfFile() 173 | let lines = data.split(separator: 0x0A) 174 | for lineData in lines { 175 | var lineBytes = [UInt8](lineData) 176 | let processedLine = unmetafy(&lineBytes) 177 | zshHistoryContent += "\(processedLine)\n" 178 | } 179 | return zshHistoryContent 180 | } 181 | return nil 182 | } 183 | 184 | private var readLastLine: String? { 185 | get throws { 186 | guard let fileHandle = try? FileHandle(forReadingFrom: self) else { 187 | return nil 188 | } 189 | defer { fileHandle.closeFile() } 190 | 191 | var offset = fileHandle.seekToEndOfFile() 192 | var lineData = Data() 193 | 194 | while offset > 0 { 195 | offset -= 1 196 | fileHandle.seek(toFileOffset: offset) 197 | let data = fileHandle.readData(ofLength: 1) 198 | 199 | if let character = String(data: data, encoding: .utf8), character == "\n" { 200 | if !lineData.isEmpty { 201 | break 202 | } 203 | } else { 204 | lineData.insert(contentsOf: data, at: 0) 205 | } 206 | } 207 | 208 | if offset == 0 && lineData.isEmpty { 209 | fileHandle.seek(toFileOffset: 0) 210 | lineData = fileHandle.readDataToEndOfFile() 211 | } 212 | 213 | guard let lastLine = String(data: lineData, encoding: .utf8) else { 214 | throw NSError(domain: "FileReadError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to decode UTF-8"]) 215 | } 216 | 217 | return lastLine 218 | } 219 | } 220 | 221 | private var readLastLineZ: String? { 222 | func unmetafy(_ bytes: inout [UInt8]) -> String { 223 | var index = 0 224 | let zshMeta: UInt8 = 0x83 225 | 226 | while index < bytes.count { 227 | if bytes[index] == zshMeta { 228 | bytes.remove(at: index) 229 | if index < bytes.count { bytes[index] ^= 32 } 230 | } else { 231 | index += 1 232 | } 233 | } 234 | return String(decoding: bytes, as: UTF8.self) 235 | } 236 | 237 | guard let fileHandle = FileHandle(forReadingAtPath: self.path) else { return nil } 238 | defer { fileHandle.closeFile() } 239 | 240 | let bufferSize = 1024 241 | var offset = fileHandle.seekToEndOfFile() 242 | var lastLineData = Data() 243 | 244 | while offset > 0 { 245 | let bytesToRead = min(offset, UInt64(bufferSize)) 246 | offset -= bytesToRead 247 | fileHandle.seek(toFileOffset: offset) 248 | var data = fileHandle.readData(ofLength: Int(bytesToRead)) 249 | if data.last == 0x0A { data = data.dropLast() } 250 | if let range = data.range(of: Data([0x0A]), options: .backwards) { 251 | lastLineData = data.suffix(from: range.upperBound) + lastLineData 252 | break 253 | } else { 254 | lastLineData = data + lastLineData 255 | } 256 | } 257 | 258 | if lastLineData.isEmpty { 259 | fileHandle.seek(toFileOffset: 0) 260 | lastLineData = fileHandle.readDataToEndOfFile() 261 | } 262 | 263 | //lastLineData.append(0x0A) 264 | if lastLineData.isEmpty { return nil } 265 | 266 | var lineBytes = [UInt8](lastLineData) 267 | let processedLine = unmetafy(&lineBytes) 268 | return "\(processedLine)\n" 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | xHistory 5 | 6 | v0.1.9 7 | Fri, 13 Jun 2025 18:02:58 +0800 8 | 19 9 | 0.1.9 10 | 12.0 11 | ul{margin-top: 0;margin-bottom: 7;padding-left: 18;}
    13 |
  • 修复了对搜索结果使用智能切片时内容错误的问题.
  • 14 |
  • 更新了开发者证书.
  • 15 |
    16 |
  • Fixed: Using "Magic Slice" on search results would result in incorrect content.
  • 17 |
  • Refreshed the developer certificate.
  • 18 |
19 | ]]>
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | v0.1.8 29 | Wed, 11 Jun 2025 17:56:04 +0800 30 | 18 31 | 0.1.8 32 | 12.0 33 | ul{margin-top: 0;margin-bottom: 7;padding-left: 18;}
    35 |
  • 支持编辑已收藏的命令文本.
  • 36 |
  • 我的免费开发者证书将在6月14日过期, 请收藏 xHistory 的主页以随时下载最新版
  • 37 |
    38 |
  • Support editing of pinned commands.
  • 39 |
  • My certificate will expire on June 14, bookmark this page to download xHistory at any time! 40 |
  • 41 |
42 | ]]>
43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 51 | v0.1.6 52 | Tue, 07 Jan 2025 11:19:41 +0800 53 | 16 54 | 0.1.6 55 | 12.0 56 | ul{margin-top: 0;margin-bottom: 7;padding-left: 18;}
    58 |
  • 修复了部分已知的问题.
  • 59 |
  • 添加了 zh-Hant 本地化翻译.
  • 60 |
    61 |
  • Fixed some known issues.
  • 62 |
  • Added zh-Hant localization.
  • 63 |
64 | ]]>
65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 | 73 | v0.1.5 74 | Sun, 24 Nov 2024 00:16:44 +0800 75 | 15 76 | 0.1.5 77 | 12.0 78 | ul{margin-top: 0;margin-bottom: 7;padding-left: 18;}
    80 |
  • 修复了多显示器场景下, 叠加层窗口可能偏离正确位置的问题.
  • 81 |
    82 |
  • Fixed: Overlay window position could become offset when using multiple monitors.
  • 83 |
84 | ]]>
85 | 86 | 87 | 88 | 89 | 90 | 91 |
92 | 93 | v0.1.4 94 | Fri, 15 Nov 2024 21:42:33 +0800 95 | 14 96 | 0.1.4 97 | 12.0 98 | ul{margin-top: 0;margin-bottom: 7;padding-left: 18;}
    100 |
  • 支持在屏蔽列表中使用正则表达式
  • 101 |
  • 支持使用快捷键切换页面和快速填充命令
  • 102 |
    103 |
  • Supports regular expressions in block list.
  • 104 |
  • Supports using shortcut keys to switch pages and select commands.
  • 105 |
106 | ]]>
107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 | 115 | v0.1.3 116 | Thu, 14 Nov 2024 16:47:57 +0800 117 | 13 118 | 0.1.3 119 | 12.0 120 | ul{margin-top: 0;margin-bottom: 7;padding-left: 18;}
    122 |
  • 添加了云存档功能, 支持在多台 Mac 之间互相查询历史记录
  • 123 |
  • 修复了一些已知的问题
  • 124 |
    125 |
  • Support cloud archiving, you can see the command history of another Mac
  • 126 |
  • Fixed some known issues
  • 127 |
128 | ]]>
129 | 130 | 131 | 132 | 133 | 134 | 135 |
136 | 137 | v0.1.2 138 | Mon, 11 Nov 2024 23:13:14 +0800 139 | 12 140 | 0.1.2 141 | 12.0 142 | ul{margin-top: 0;margin-bottom: 7;padding-left: 18;}
    144 |
  • 修复了命令收藏功能表现异常的严重 Bug
  • 145 |
  • 修复了终端窗口全屏时, 叠加层尺寸不正常的问题
  • 146 |
  • 修复了 Zsh 偏好设置不生效的问题 (历史去重, 实时保存等)
  • 147 |
    148 |
  • Fixed: "Pin" feature not working properly
  • 149 |
  • Fixed: Wrong overlay size when terminal window is fullscreen
  • 150 |
  • Fixed: Zsh configuration not working (deduplication and real-time saving)
  • 151 |
152 | ]]>
153 | 154 | 155 | 156 | 157 | 158 |
159 | 160 | v0.1.1 161 | Mon, 11 Nov 2024 20:44:55 +0800 162 | 11 163 | 0.1.1 164 | 12.0 165 | ul{margin-top: 0;margin-bottom: 7;padding-left: 18;}
    167 |
  • 支持使用正则表达式进行命令搜索, 或进行大小写不敏感的模糊搜索.
  • 168 |
  • 打开面板后, 搜索框将被作为默认输入焦点, 可以直接开始搜索.
  • 169 |
  • 在"命令行"设置中新增了"预格式化器"功能, 允许用户使用正则表达式对历史记录进行预匹配.
  • 170 |
    171 |
  • Supports searching with regular expressions or fuzzy searching.
  • 172 |
  • After opening the panel, the cursor will be placed in the search field.
  • 173 |
  • Added a "Preformatter" feature in "Shell" settings, which allowing you to pre-match history using regex.
  • 174 |
175 | ]]>
176 | 177 | 178 | 179 | 180 |
181 | 182 | v0.1.0 183 | Sun, 10 Nov 2024 23:45:02 +0800 184 | 1 185 | 0.1.0 186 | 12.0 187 | 188 | 189 |
190 |
191 | -------------------------------------------------------------------------------- /xHistory/xHistoryApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // xHistoryApp.swift 3 | // xHistory 4 | // 5 | // Created by apple on 2024/11/5. 6 | // 7 | 8 | import AppKit 9 | import SwiftUI 10 | import Sparkle 11 | import SFSMonitor 12 | //import UserNotifications 13 | import KeyboardShortcuts 14 | import SystemConfiguration 15 | 16 | //let nc = NSWorkspace.shared.notificationCenter 17 | let ud = UserDefaults.standard 18 | let fd = FileManager.default 19 | let cloudFileExtension = "xha" 20 | let queue = SFSMonitor(delegate: HistoryCopyer.shared) 21 | let homeDirectory = FileManager.default.homeDirectoryForCurrentUser 22 | let updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) 23 | let statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 24 | let mainPanel = NSPanel(contentRect: NSRect(x: 0, y: 0, width: 550, height: 695), styleMask: [.fullSizeContentView, .resizable, .closable, .miniaturizable, .nonactivatingPanel, .titled], backing: .buffered, defer: false) 25 | let menuPopover = NSPopover() 26 | 27 | @main 28 | struct xHistoryApp: App { 29 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 30 | 31 | var body: some Scene { 32 | Settings { 33 | SettingsView() 34 | .background( 35 | WindowAccessor( 36 | onWindowOpen: { w in 37 | if let w = w { 38 | //w.level = .floating 39 | w.titlebarSeparatorStyle = .none 40 | guard let nsSplitView = findNSSplitVIew(view: w.contentView), 41 | let controller = nsSplitView.delegate as? NSSplitViewController else { return } 42 | controller.splitViewItems.first?.canCollapse = false 43 | controller.splitViewItems.first?.minimumThickness = 140 44 | controller.splitViewItems.first?.maximumThickness = 140 45 | w.orderFront(nil) 46 | } 47 | }) 48 | ) 49 | } 50 | } 51 | } 52 | 53 | class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {//, UNUserNotificationCenterDelegate { 54 | @AppStorage("statusIconName") var statusIconName = "menuBar" 55 | @AppStorage("historyFile") var historyFile = "~/.bash_history" 56 | @AppStorage("statusBar") var statusBar = true 57 | //@AppStorage("showPinned") var showPinned = false 58 | @AppStorage("buttonSide") var buttonSide = "right" 59 | @AppStorage("cloudSync") var cloudSync = false 60 | @AppStorage("cloudDirectory") var cloudDirectory = "" 61 | //@AppStorage("dockIcon") var dockIcon = false 62 | 63 | func applicationWillFinishLaunching(_ notification: Notification) { 64 | //if dockIcon { NSApp.setActivationPolicy(.regular) } 65 | updateShellConfig() 66 | ud.register(defaults: ["blockedCommands": ["xhistory"]]) 67 | #if RELEASE 68 | let bashrc = homeDirectory.appendingPathComponent(".bash_profile") 69 | let zshrc = homeDirectory.appendingPathComponent(".zshrc") 70 | try? createEmptyFile(at: bashrc) 71 | try? createEmptyFile(at: zshrc) 72 | if let resourceURL = Bundle.main.resourceURL { 73 | let bRC = bashrc.readHistory ?? "" 74 | let zRC = zshrc.readHistory ?? "" 75 | let command = resourceURL.appendingPathComponent("xh").path 76 | let bashC = "eval $(\(command) -c bash 2>/dev/null)" 77 | if !bRC.contains(bashC) { try? appendLine(to: bashrc, line: "\n\(bashC)") } 78 | let zshC1 = "eval $(\(command) -c zsh 2>/dev/null)" 79 | if !zRC.contains(zshC1) { try? appendLine(to: zshrc, line: "\n\(zshC1)") } 80 | let zshC2 = "$(\(command) -c zsh2 2>/dev/null)" 81 | let zshC3 = "$(\(command) -c zsh3 2>/dev/null)" 82 | if !zRC.contains(zshC2) { try? appendLine(to: zshrc, line: "\n\(zshC2)") } 83 | if !zRC.contains(zshC3) { try? appendLine(to: zshrc, line: "\n\(zshC3)") } 84 | } 85 | #endif 86 | NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(handleURLEvent(_:replyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)) 87 | 88 | /*UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in 89 | if let error = error { print("⚠️ Notification authorization denied: \(error.localizedDescription)") } 90 | } 91 | UNUserNotificationCenter.current().delegate = self*/ 92 | //KeyboardShortcuts.onKeyDown(for: .swapButtons) { self.swapButtons.toggle() } 93 | KeyboardShortcuts.onKeyDown(for: .showPanel) { self.openMainPanel() } 94 | KeyboardShortcuts.onKeyDown(for: .showOverlay) { openCustomURLWithActiveWindowGeometry() } 95 | KeyboardShortcuts.onKeyDown(for: .showPinnedPanel) { 96 | PageState.shared.pageID = 2 97 | self.openMainPanel() 98 | } 99 | KeyboardShortcuts.onKeyDown(for: .showPinnedOverlay) { 100 | PageState.shared.pageID = 2 101 | openCustomURLWithActiveWindowGeometry() 102 | } 103 | 104 | if let button = statusBarItem.button { 105 | button.target = self 106 | button.image = NSImage(named: statusIconName) 107 | button.action = #selector(togglePopover(_ :)) 108 | menuPopover.contentSize = NSSize(width: 500, height: 352) 109 | menuPopover.setValue(true, forKeyPath: "shouldHideAnchor") 110 | menuPopover.behavior = .transient 111 | } 112 | statusBarItem.isVisible = statusBar 113 | //swapButtons = false 114 | if !fd.fileExists(atPath: historyFile.absolutePath) { 115 | if historyFile == "~/.bash_history" { 116 | historyFile = "~/.zsh_history" 117 | } else if historyFile == "~/.zsh_history" { 118 | historyFile = "~/.bash_history" 119 | } 120 | } 121 | } 122 | 123 | @objc func togglePopover(_ sender: Any?) { 124 | if let button = statusBarItem.button, !menuPopover.isShown { 125 | menuPopover.contentViewController = NSHostingController(rootView: ContentView(fromMenubar: true)) 126 | var bound = button.bounds 127 | if getMenuBarHeight() == 24.0 { bound.origin.y -= 6 } 128 | menuPopover.show(relativeTo: bound, of: button, preferredEdge: .minY) 129 | menuPopover.contentViewController?.view.window?.makeKeyAndOrderFront(nil) 130 | mainPanel.close() 131 | } 132 | } 133 | 134 | func openMainPanel(file: String? = nil) { 135 | HistoryCopyer.shared.updateHistory(file: file) 136 | mainPanel.setFrame(NSRect(x: 0, y: 0, width: 550, height: 695), display: true) 137 | mainPanel.center() 138 | mainPanel.makeKeyAndOrderFront(self) 139 | menuPopover.performClose(self) 140 | } 141 | 142 | func applicationDidFinishLaunching(_ notification: Notification) { 143 | queue?.setMaxMonitored(number: 200) 144 | _ = queue?.addURL(historyFile.absolutePath.url) 145 | 146 | for cmd in HistoryCopyer.shared.readHistory() { 147 | SyntaxHighlighter.shared.getHighlightedTextAsync(for: cmd) { _ in } 148 | } 149 | 150 | mainPanel.title = "xHistory Panel".local 151 | mainPanel.level = .floating 152 | mainPanel.isOpaque = false 153 | //mainPanel.hasShadow = false 154 | mainPanel.titleVisibility = .hidden 155 | mainPanel.titlebarAppearsTransparent = true 156 | mainPanel.isReleasedWhenClosed = false 157 | mainPanel.isMovableByWindowBackground = true 158 | mainPanel.becomesKeyOnlyIfNeeded = true 159 | mainPanel.backgroundColor = .clear 160 | mainPanel.collectionBehavior = [.canJoinAllSpaces] 161 | let contentView = NSHostingView(rootView: ContentView()) 162 | contentView.focusRingType = .none 163 | mainPanel.contentView = contentView 164 | 165 | tips("You can click on any command or slice\nto fill it into the lower window\n(accessibility permissions required)".local, 166 | id: "xh.how-to-use.note") 167 | tips("If your history has extras (like timestamps)\nyou can preformat it with a custom Regex in\n\"Preferences\" > \"Shell\" > \"Preformatter\"".local, 168 | id: "xh.pre-formatter.note", width:260) 169 | if !CommandLineTool.isInstalled() { 170 | tips("Do you want to install the command line tool?\nAfter installation, you can run \"xhistory\" in yor terminal to quickly open the floating panel.\n\n(You can also install it later in preferences)".local, 171 | title: "Command Line Tool".local, 172 | id: "xh.install-clt.note", switchButton: true, width: 250) { 173 | CommandLineTool.install() 174 | } 175 | } 176 | } 177 | 178 | func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool { 179 | self.openMainPanel() 180 | return false 181 | } 182 | 183 | func closeAllWindow(except: String = "") { 184 | for w in NSApp.windows.filter({ 185 | $0.title != "Item-0" && $0.title != "" 186 | && !$0.title.contains(except) }) { w.close() } 187 | } 188 | 189 | @objc func handleURLEvent(_ event: NSAppleEventDescriptor, replyEvent: NSAppleEventDescriptor) { 190 | if let urlString = event.paramDescriptor(forKeyword: AEKeyword(keyDirectObject))?.stringValue, 191 | let url = URL(string: urlString) { 192 | if url.scheme == "xhistory"{ 193 | switch url.host { 194 | case "show" : 195 | guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return } 196 | let queryItems = components.queryItems 197 | if queryItems == nil { self.openMainPanel() } 198 | if let x = queryItems?.first(where: { $0.name == "x" })?.value, 199 | let y = queryItems?.first(where: { $0.name == "y" })?.value, 200 | let w = queryItems?.first(where: { $0.name == "w" })?.value, 201 | let h = queryItems?.first(where: { $0.name == "h" })?.value { 202 | if let xInt = Int(x), let yInt = Int(y), let wInt = Int(w), let hInt = Int(h) { 203 | let file = queryItems?.first(where: { $0.name == "file" })?.value 204 | openMainPanel(file: file) 205 | let bound = CGRectTransform(cgRect: CGRect(x: xInt, y: yInt + 28, width: wInt, height: hInt - 28)) 206 | mainPanel.setFrame(bound, display: true) 207 | if let mode = queryItems?.first(where: { $0.name == "mode" })?.value { 208 | switch mode { 209 | case "pinned": PageState.shared.pageID = 2 210 | case "archive": 211 | if cloudSync && cloudDirectory != "" { 212 | PageState.shared.pageID = 3 213 | } 214 | default: PageState.shared.pageID = 1 215 | } 216 | } 217 | } 218 | } 219 | default: print("Unknow command!") 220 | } 221 | } 222 | } 223 | } 224 | } 225 | 226 | func findNSSplitVIew(view: NSView?) -> NSSplitView? { 227 | var queue = [NSView]() 228 | if let root = view { queue.append(root) } 229 | 230 | while !queue.isEmpty { 231 | let current = queue.removeFirst() 232 | if current is NSSplitView { return current as? NSSplitView } 233 | for subview in current.subviews { queue.append(subview) } 234 | } 235 | return nil 236 | } 237 | 238 | func CGRectTransform(cgRect: CGRect) -> NSRect { 239 | let x = cgRect.origin.x 240 | let y = cgRect.origin.y 241 | let w = cgRect.width 242 | let h = cgRect.height 243 | if let main = NSScreen.screens.first(where: { $0.isMainScreen }) { 244 | return NSRect(x: x, y: main.frame.height - y - h, width: w, height: h) 245 | } 246 | return cgRect 247 | } 248 | 249 | func openSettingPanel() { 250 | NSApp.activate(ignoringOtherApps: true) 251 | if #available(macOS 14, *) { 252 | NSApp.mainMenu?.items.first?.submenu?.item(at: 2)?.performAction() 253 | }else if #available(macOS 13, *) { 254 | NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) 255 | } else { 256 | NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) 257 | } 258 | } 259 | 260 | func openAboutPanel() { 261 | NSApp.activate(ignoringOtherApps: true) 262 | NSApp.orderFrontStandardAboutPanel(nil) 263 | } 264 | 265 | func getMenuBarHeight() -> CGFloat { 266 | let mouseLocation = NSEvent.mouseLocation 267 | let screen = NSScreen.screens.first(where: { NSMouseInRect(mouseLocation, $0.frame, false) }) 268 | if let screen = screen { 269 | return screen.frame.height - screen.visibleFrame.height - (screen.visibleFrame.origin.y - screen.frame.origin.y) - 1 270 | } 271 | return 0.0 272 | } 273 | 274 | func getMacDeviceName() -> String { 275 | @AppStorage("machineType") var machineType = "mac" 276 | var computerName: CFString? 277 | if let dynamicStore = SCDynamicStoreCreate(nil, "GetComputerName" as CFString, nil, nil) { 278 | computerName = SCDynamicStoreCopyComputerName(dynamicStore, nil) as CFString? 279 | } 280 | if let name = computerName as String? { return name } 281 | return machineType 282 | } 283 | 284 | func getMacDeviceUUID() -> String? { 285 | let dev = IOServiceMatching("IOPlatformExpertDevice") 286 | let platformExpert: io_service_t = IOServiceGetMatchingService(kIOMainPortDefault, dev) 287 | if platformExpert != 0 { 288 | if let serialNumberAsCFString = IORegistryEntryCreateCFProperty(platformExpert, kIOPlatformUUIDKey as CFString, kCFAllocatorDefault, 0)?.takeUnretainedValue() { 289 | IOObjectRelease(platformExpert) 290 | return serialNumberAsCFString as? String 291 | } 292 | IOObjectRelease(platformExpert) 293 | } 294 | return nil 295 | } 296 | 297 | func createEmptyFile(at fileURL: URL) throws { 298 | if !fd.fileExists(atPath: fileURL.path) { 299 | do { 300 | try fd.createDirectory(at: fileURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) 301 | fd.createFile(atPath: fileURL.path, contents: nil, attributes: nil) 302 | } catch { 303 | print("Cannot create data file: \(error)") 304 | } 305 | } 306 | } 307 | 308 | func appendLine(to fileURL: URL, line: String, encoding: String.Encoding = .utf8) throws { 309 | let newLine = line + "\n" 310 | 311 | if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) { 312 | defer { fileHandle.closeFile() } 313 | fileHandle.seekToEndOfFile() 314 | if let data = newLine.data(using: .utf8) { fileHandle.write(data) } 315 | } else { 316 | try newLine.write(to: fileURL, atomically: true, encoding: encoding) 317 | } 318 | } 319 | 320 | func updateShellConfig() { 321 | @AppStorage("customShellConfig") var customShellConfig = true 322 | @AppStorage("historyLimit") var historyLimit = 1000 323 | @AppStorage("noDuplicates") var noDuplicates = true 324 | @AppStorage("realtimeSave") var realtimeSave = true 325 | 326 | let data: [String: Any] = [ 327 | "customShellConfig": customShellConfig, 328 | "historyLimit": historyLimit, 329 | "noDuplicates": noDuplicates, 330 | "realtimeSave": realtimeSave 331 | ] 332 | if let appSupportDir = fd.urls(for: .applicationSupportDirectory, in: .userDomainMask).first { 333 | let appFolder = appSupportDir.appendingPathComponent(Bundle.main.appName) 334 | let plistURL = appFolder.appendingPathComponent("shellConfig.plist") 335 | if !fd.fileExists(atPath: appFolder.path) { 336 | try? fd.createDirectory(at: appFolder, withIntermediateDirectories: true, attributes: nil) 337 | } 338 | let plistData = try? PropertyListSerialization.data(fromPropertyList: data, format: .xml, options: 0) 339 | try? plistData?.write(to: plistURL) 340 | } 341 | } 342 | 343 | func tips(_ message: String, title: String? = nil, id: String, switchButton: Bool = false, width: Int? = nil, action: (() -> Void)? = nil) { 344 | let never = (ud.object(forKey: "neverRemindMe") as? [String]) ?? [] 345 | if !never.contains(id) { 346 | if switchButton { 347 | let alert = createAlert(title: title ?? "xHistory Tips".local, message: message, button1: "OK", button2: "Don't remind me again", width: width).runModal() 348 | if alert == .alertSecondButtonReturn { ud.setValue(never + [id], forKey: "neverRemindMe") } 349 | if alert == .alertFirstButtonReturn { action?() } 350 | } else { 351 | let alert = createAlert(title: title ?? "xHistory Tips".local, message: message, button1: "Don't remind me again", button2: "OK", width: width).runModal() 352 | if alert == .alertFirstButtonReturn { ud.setValue(never + [id], forKey: "neverRemindMe") } 353 | if alert == .alertSecondButtonReturn { action?() } 354 | } 355 | } 356 | } 357 | 358 | func createAlert(level: NSAlert.Style = .warning, title: String, message: String, button1: String, button2: String = "", width: Int? = nil) -> NSAlert { 359 | let alert = NSAlert() 360 | alert.messageText = title.local 361 | alert.informativeText = message.local 362 | alert.addButton(withTitle: button1.local) 363 | if button2 != "" { alert.addButton(withTitle: button2.local) } 364 | alert.alertStyle = level 365 | if let width = width { 366 | alert.accessoryView = NSView(frame: NSMakeRect(0, 0, Double(width), 0)) 367 | } 368 | return alert 369 | } 370 | 371 | extension Bundle { 372 | var appName: String { 373 | let appName = self.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String 374 | ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String 375 | ?? "Unknown App Name" 376 | return appName 377 | } 378 | } 379 | 380 | extension String { 381 | var local: String { return NSLocalizedString(self, comment: "") } 382 | var deletingPathExtension: String { 383 | return (self as NSString).deletingPathExtension 384 | } 385 | var url: URL { return URL(fileURLWithPath: self) } 386 | var forceCharWrapping: Self { 387 | self.map({ String($0) }).joined(separator: "\u{200B}") 388 | } 389 | var absolutePath: String { 390 | return (self as NSString).expandingTildeInPath 391 | } 392 | func startsWith(character: Character) -> Bool { 393 | guard let firstChar = self.first else { 394 | return false 395 | } 396 | return firstChar == character 397 | } 398 | } 399 | 400 | extension NSMenuItem { 401 | func performAction() { 402 | guard let menu else { 403 | return 404 | } 405 | menu.performActionForItem(at: menu.index(of: self)) 406 | } 407 | } 408 | 409 | extension NSScreen { 410 | var displayID: CGDirectDisplayID? { 411 | return deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID 412 | } 413 | var isMainScreen: Bool { 414 | guard let id = self.displayID else { return false } 415 | return (CGDisplayIsMain(id) == 1) 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /xHistory.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 77; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 18801ECB2CDDA1EF007AA6E8 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 18801ECA2CDDA1EF007AA6E8 /* Sparkle */; }; 11 | 18801ECE2CDDA1F9007AA6E8 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 18801ECD2CDDA1F9007AA6E8 /* KeyboardShortcuts */; }; 12 | 18801ED12CDDA216007AA6E8 /* SwiftTreeSitter in Frameworks */ = {isa = PBXBuildFile; productRef = 18801ED02CDDA216007AA6E8 /* SwiftTreeSitter */; }; 13 | 18801ED32CDDA216007AA6E8 /* SwiftTreeSitterLayer in Frameworks */ = {isa = PBXBuildFile; productRef = 18801ED22CDDA216007AA6E8 /* SwiftTreeSitterLayer */; }; 14 | 18801ED62CDDA221007AA6E8 /* TreeSitterBash in Frameworks */ = {isa = PBXBuildFile; productRef = 18801ED52CDDA221007AA6E8 /* TreeSitterBash */; }; 15 | 18801EDC2CDDA249007AA6E8 /* SFSMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = 18801EDB2CDDA249007AA6E8 /* SFSMonitor */; }; 16 | 18801F342CDDA3BD007AA6E8 /* xh in CopyFiles */ = {isa = PBXBuildFile; fileRef = 18801F142CDDA38A007AA6E8 /* xh */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 17 | 18DEBA222CDF448B00A7EDED /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 18DEBA212CDF448B00A7EDED /* ArgumentParser */; }; 18 | 18EC67362CDDDC5A00B5B1F9 /* MatrixColorSelector in Frameworks */ = {isa = PBXBuildFile; productRef = 18EC67352CDDDC5A00B5B1F9 /* MatrixColorSelector */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXCopyFilesBuildPhase section */ 22 | 18801F122CDDA38A007AA6E8 /* CopyFiles */ = { 23 | isa = PBXCopyFilesBuildPhase; 24 | buildActionMask = 2147483647; 25 | dstPath = /usr/share/man/man1/; 26 | dstSubfolderSpec = 0; 27 | files = ( 28 | ); 29 | runOnlyForDeploymentPostprocessing = 1; 30 | }; 31 | 18801F332CDDA3B7007AA6E8 /* CopyFiles */ = { 32 | isa = PBXCopyFilesBuildPhase; 33 | buildActionMask = 2147483647; 34 | dstPath = ""; 35 | dstSubfolderSpec = 7; 36 | files = ( 37 | 18801F342CDDA3BD007AA6E8 /* xh in CopyFiles */, 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXCopyFilesBuildPhase section */ 42 | 43 | /* Begin PBXFileReference section */ 44 | 18801EB52CDDA160007AA6E8 /* xHistory.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = xHistory.app; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 18801F142CDDA38A007AA6E8 /* xh */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = xh; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | /* End PBXFileReference section */ 47 | 48 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 49 | 18801EC82CDDA1C0007AA6E8 /* Exceptions for "xHistory" folder in "xHistory" target */ = { 50 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 51 | membershipExceptions = ( 52 | Info.plist, 53 | ); 54 | target = 18801EB42CDDA160007AA6E8 /* xHistory */; 55 | }; 56 | 18EC67332CDDD87800B5B1F9 /* Exceptions for "xh" folder in "xHistory" target */ = { 57 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; 58 | membershipExceptions = ( 59 | MakeOverlay.swift, 60 | ); 61 | target = 18801EB42CDDA160007AA6E8 /* xHistory */; 62 | }; 63 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ 64 | 65 | /* Begin PBXFileSystemSynchronizedRootGroup section */ 66 | 18801EB72CDDA160007AA6E8 /* xHistory */ = { 67 | isa = PBXFileSystemSynchronizedRootGroup; 68 | exceptions = ( 69 | 18801EC82CDDA1C0007AA6E8 /* Exceptions for "xHistory" folder in "xHistory" target */, 70 | ); 71 | path = xHistory; 72 | sourceTree = ""; 73 | }; 74 | 18801F152CDDA38B007AA6E8 /* xh */ = { 75 | isa = PBXFileSystemSynchronizedRootGroup; 76 | exceptions = ( 77 | 18EC67332CDDD87800B5B1F9 /* Exceptions for "xh" folder in "xHistory" target */, 78 | ); 79 | path = xh; 80 | sourceTree = ""; 81 | }; 82 | /* End PBXFileSystemSynchronizedRootGroup section */ 83 | 84 | /* Begin PBXFrameworksBuildPhase section */ 85 | 18801EB22CDDA160007AA6E8 /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | 18EC67362CDDDC5A00B5B1F9 /* MatrixColorSelector in Frameworks */, 90 | 18801ED12CDDA216007AA6E8 /* SwiftTreeSitter in Frameworks */, 91 | 18801ED32CDDA216007AA6E8 /* SwiftTreeSitterLayer in Frameworks */, 92 | 18801ED62CDDA221007AA6E8 /* TreeSitterBash in Frameworks */, 93 | 18801ECB2CDDA1EF007AA6E8 /* Sparkle in Frameworks */, 94 | 18801ECE2CDDA1F9007AA6E8 /* KeyboardShortcuts in Frameworks */, 95 | 18801EDC2CDDA249007AA6E8 /* SFSMonitor in Frameworks */, 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | 18801F112CDDA38A007AA6E8 /* Frameworks */ = { 100 | isa = PBXFrameworksBuildPhase; 101 | buildActionMask = 2147483647; 102 | files = ( 103 | 18DEBA222CDF448B00A7EDED /* ArgumentParser in Frameworks */, 104 | ); 105 | runOnlyForDeploymentPostprocessing = 0; 106 | }; 107 | /* End PBXFrameworksBuildPhase section */ 108 | 109 | /* Begin PBXGroup section */ 110 | 18801EAC2CDDA160007AA6E8 = { 111 | isa = PBXGroup; 112 | children = ( 113 | 18801EB72CDDA160007AA6E8 /* xHistory */, 114 | 18801F152CDDA38B007AA6E8 /* xh */, 115 | 18801EB62CDDA160007AA6E8 /* Products */, 116 | ); 117 | sourceTree = ""; 118 | }; 119 | 18801EB62CDDA160007AA6E8 /* Products */ = { 120 | isa = PBXGroup; 121 | children = ( 122 | 18801EB52CDDA160007AA6E8 /* xHistory.app */, 123 | 18801F142CDDA38A007AA6E8 /* xh */, 124 | ); 125 | name = Products; 126 | sourceTree = ""; 127 | }; 128 | /* End PBXGroup section */ 129 | 130 | /* Begin PBXNativeTarget section */ 131 | 18801EB42CDDA160007AA6E8 /* xHistory */ = { 132 | isa = PBXNativeTarget; 133 | buildConfigurationList = 18801EC42CDDA161007AA6E8 /* Build configuration list for PBXNativeTarget "xHistory" */; 134 | buildPhases = ( 135 | 18801EB12CDDA160007AA6E8 /* Sources */, 136 | 18801EB22CDDA160007AA6E8 /* Frameworks */, 137 | 18801EB32CDDA160007AA6E8 /* Resources */, 138 | 18801F332CDDA3B7007AA6E8 /* CopyFiles */, 139 | ); 140 | buildRules = ( 141 | ); 142 | dependencies = ( 143 | ); 144 | fileSystemSynchronizedGroups = ( 145 | 18801EB72CDDA160007AA6E8 /* xHistory */, 146 | ); 147 | name = xHistory; 148 | packageProductDependencies = ( 149 | 18801ECA2CDDA1EF007AA6E8 /* Sparkle */, 150 | 18801ECD2CDDA1F9007AA6E8 /* KeyboardShortcuts */, 151 | 18801ED02CDDA216007AA6E8 /* SwiftTreeSitter */, 152 | 18801ED22CDDA216007AA6E8 /* SwiftTreeSitterLayer */, 153 | 18801ED52CDDA221007AA6E8 /* TreeSitterBash */, 154 | 18801EDB2CDDA249007AA6E8 /* SFSMonitor */, 155 | 18EC67352CDDDC5A00B5B1F9 /* MatrixColorSelector */, 156 | ); 157 | productName = xHistory; 158 | productReference = 18801EB52CDDA160007AA6E8 /* xHistory.app */; 159 | productType = "com.apple.product-type.application"; 160 | }; 161 | 18801F132CDDA38A007AA6E8 /* xh */ = { 162 | isa = PBXNativeTarget; 163 | buildConfigurationList = 18801F182CDDA38B007AA6E8 /* Build configuration list for PBXNativeTarget "xh" */; 164 | buildPhases = ( 165 | 18801F102CDDA38A007AA6E8 /* Sources */, 166 | 18801F112CDDA38A007AA6E8 /* Frameworks */, 167 | 18801F122CDDA38A007AA6E8 /* CopyFiles */, 168 | ); 169 | buildRules = ( 170 | ); 171 | dependencies = ( 172 | ); 173 | fileSystemSynchronizedGroups = ( 174 | 18801F152CDDA38B007AA6E8 /* xh */, 175 | ); 176 | name = xh; 177 | packageProductDependencies = ( 178 | 18DEBA212CDF448B00A7EDED /* ArgumentParser */, 179 | ); 180 | productName = xh; 181 | productReference = 18801F142CDDA38A007AA6E8 /* xh */; 182 | productType = "com.apple.product-type.tool"; 183 | }; 184 | /* End PBXNativeTarget section */ 185 | 186 | /* Begin PBXProject section */ 187 | 18801EAD2CDDA160007AA6E8 /* Project object */ = { 188 | isa = PBXProject; 189 | attributes = { 190 | BuildIndependentTargetsInParallel = 1; 191 | LastSwiftUpdateCheck = 1600; 192 | LastUpgradeCheck = 1620; 193 | TargetAttributes = { 194 | 18801EB42CDDA160007AA6E8 = { 195 | CreatedOnToolsVersion = 16.0; 196 | }; 197 | 18801F132CDDA38A007AA6E8 = { 198 | CreatedOnToolsVersion = 16.0; 199 | }; 200 | }; 201 | }; 202 | buildConfigurationList = 18801EB02CDDA160007AA6E8 /* Build configuration list for PBXProject "xHistory" */; 203 | developmentRegion = en; 204 | hasScannedForEncodings = 0; 205 | knownRegions = ( 206 | en, 207 | Base, 208 | "zh-Hans", 209 | "zh-Hant", 210 | ); 211 | mainGroup = 18801EAC2CDDA160007AA6E8; 212 | minimizedProjectReferenceProxies = 1; 213 | packageReferences = ( 214 | 18801EC92CDDA1EF007AA6E8 /* XCRemoteSwiftPackageReference "Sparkle" */, 215 | 18801ECC2CDDA1F9007AA6E8 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, 216 | 18801ECF2CDDA216007AA6E8 /* XCRemoteSwiftPackageReference "SwiftTreeSitter" */, 217 | 18801ED42CDDA221007AA6E8 /* XCRemoteSwiftPackageReference "tree-sitter-bash" */, 218 | 18801EDA2CDDA249007AA6E8 /* XCRemoteSwiftPackageReference "SFSMonitor" */, 219 | 18EC67342CDDDC5A00B5B1F9 /* XCRemoteSwiftPackageReference "MatrixColorSelector" */, 220 | 18DEBA202CDF448B00A7EDED /* XCRemoteSwiftPackageReference "swift-argument-parser" */, 221 | ); 222 | preferredProjectObjectVersion = 77; 223 | productRefGroup = 18801EB62CDDA160007AA6E8 /* Products */; 224 | projectDirPath = ""; 225 | projectRoot = ""; 226 | targets = ( 227 | 18801EB42CDDA160007AA6E8 /* xHistory */, 228 | 18801F132CDDA38A007AA6E8 /* xh */, 229 | ); 230 | }; 231 | /* End PBXProject section */ 232 | 233 | /* Begin PBXResourcesBuildPhase section */ 234 | 18801EB32CDDA160007AA6E8 /* Resources */ = { 235 | isa = PBXResourcesBuildPhase; 236 | buildActionMask = 2147483647; 237 | files = ( 238 | ); 239 | runOnlyForDeploymentPostprocessing = 0; 240 | }; 241 | /* End PBXResourcesBuildPhase section */ 242 | 243 | /* Begin PBXSourcesBuildPhase section */ 244 | 18801EB12CDDA160007AA6E8 /* Sources */ = { 245 | isa = PBXSourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | 18801F102CDDA38A007AA6E8 /* Sources */ = { 252 | isa = PBXSourcesBuildPhase; 253 | buildActionMask = 2147483647; 254 | files = ( 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | /* End PBXSourcesBuildPhase section */ 259 | 260 | /* Begin XCBuildConfiguration section */ 261 | 18801EC22CDDA161007AA6E8 /* Debug */ = { 262 | isa = XCBuildConfiguration; 263 | buildSettings = { 264 | ALWAYS_SEARCH_USER_PATHS = NO; 265 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 266 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 267 | CLANG_ANALYZER_NONNULL = YES; 268 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 269 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 270 | CLANG_ENABLE_MODULES = YES; 271 | CLANG_ENABLE_OBJC_ARC = YES; 272 | CLANG_ENABLE_OBJC_WEAK = YES; 273 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 274 | CLANG_WARN_BOOL_CONVERSION = YES; 275 | CLANG_WARN_COMMA = YES; 276 | CLANG_WARN_CONSTANT_CONVERSION = YES; 277 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 278 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 279 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 280 | CLANG_WARN_EMPTY_BODY = YES; 281 | CLANG_WARN_ENUM_CONVERSION = YES; 282 | CLANG_WARN_INFINITE_RECURSION = YES; 283 | CLANG_WARN_INT_CONVERSION = YES; 284 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 285 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 286 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 287 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 288 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 289 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 290 | CLANG_WARN_STRICT_PROTOTYPES = YES; 291 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 292 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 293 | CLANG_WARN_UNREACHABLE_CODE = YES; 294 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 295 | COPY_PHASE_STRIP = NO; 296 | DEAD_CODE_STRIPPING = YES; 297 | DEBUG_INFORMATION_FORMAT = dwarf; 298 | ENABLE_STRICT_OBJC_MSGSEND = YES; 299 | ENABLE_TESTABILITY = YES; 300 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 301 | GCC_C_LANGUAGE_STANDARD = gnu17; 302 | GCC_DYNAMIC_NO_PIC = NO; 303 | GCC_NO_COMMON_BLOCKS = YES; 304 | GCC_OPTIMIZATION_LEVEL = 0; 305 | GCC_PREPROCESSOR_DEFINITIONS = ( 306 | "DEBUG=1", 307 | "$(inherited)", 308 | ); 309 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 310 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 311 | GCC_WARN_UNDECLARED_SELECTOR = YES; 312 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 313 | GCC_WARN_UNUSED_FUNCTION = YES; 314 | GCC_WARN_UNUSED_VARIABLE = YES; 315 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 316 | MACOSX_DEPLOYMENT_TARGET = 15.0; 317 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 318 | MTL_FAST_MATH = YES; 319 | ONLY_ACTIVE_ARCH = YES; 320 | SDKROOT = macosx; 321 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 322 | SWIFT_EMIT_LOC_STRINGS = YES; 323 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 324 | }; 325 | name = Debug; 326 | }; 327 | 18801EC32CDDA161007AA6E8 /* Release */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ALWAYS_SEARCH_USER_PATHS = NO; 331 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 332 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 333 | CLANG_ANALYZER_NONNULL = YES; 334 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 335 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 336 | CLANG_ENABLE_MODULES = YES; 337 | CLANG_ENABLE_OBJC_ARC = YES; 338 | CLANG_ENABLE_OBJC_WEAK = YES; 339 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 340 | CLANG_WARN_BOOL_CONVERSION = YES; 341 | CLANG_WARN_COMMA = YES; 342 | CLANG_WARN_CONSTANT_CONVERSION = YES; 343 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 344 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 345 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 346 | CLANG_WARN_EMPTY_BODY = YES; 347 | CLANG_WARN_ENUM_CONVERSION = YES; 348 | CLANG_WARN_INFINITE_RECURSION = YES; 349 | CLANG_WARN_INT_CONVERSION = YES; 350 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 351 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 352 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 353 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 354 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 355 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 356 | CLANG_WARN_STRICT_PROTOTYPES = YES; 357 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 358 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 359 | CLANG_WARN_UNREACHABLE_CODE = YES; 360 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 361 | COPY_PHASE_STRIP = NO; 362 | DEAD_CODE_STRIPPING = YES; 363 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 364 | ENABLE_NS_ASSERTIONS = NO; 365 | ENABLE_STRICT_OBJC_MSGSEND = YES; 366 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 367 | GCC_C_LANGUAGE_STANDARD = gnu17; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 376 | MACOSX_DEPLOYMENT_TARGET = 15.0; 377 | MTL_ENABLE_DEBUG_INFO = NO; 378 | MTL_FAST_MATH = YES; 379 | SDKROOT = macosx; 380 | SWIFT_COMPILATION_MODE = wholemodule; 381 | SWIFT_EMIT_LOC_STRINGS = YES; 382 | }; 383 | name = Release; 384 | }; 385 | 18801EC52CDDA161007AA6E8 /* Debug */ = { 386 | isa = XCBuildConfiguration; 387 | buildSettings = { 388 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 389 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 390 | CODE_SIGN_ENTITLEMENTS = xHistory/xHistory.entitlements; 391 | CODE_SIGN_STYLE = Automatic; 392 | COMBINE_HIDPI_IMAGES = YES; 393 | CURRENT_PROJECT_VERSION = 19; 394 | DEAD_CODE_STRIPPING = YES; 395 | DEVELOPMENT_ASSET_PATHS = "\"xHistory/Preview Content\""; 396 | DEVELOPMENT_TEAM = L4T783637F; 397 | ENABLE_HARDENED_RUNTIME = YES; 398 | ENABLE_PREVIEWS = YES; 399 | GENERATE_INFOPLIST_FILE = YES; 400 | INFOPLIST_FILE = xHistory/Info.plist; 401 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 402 | INFOPLIST_KEY_LSUIElement = YES; 403 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 lihaoyun6. All rights reserved."; 404 | LD_RUNPATH_SEARCH_PATHS = ( 405 | "$(inherited)", 406 | "@executable_path/../Frameworks", 407 | ); 408 | MACOSX_DEPLOYMENT_TARGET = 12.0; 409 | MARKETING_VERSION = 0.1.9; 410 | PRODUCT_BUNDLE_IDENTIFIER = com.lihaoyun6.xHistory; 411 | PRODUCT_NAME = "$(TARGET_NAME)"; 412 | SWIFT_EMIT_LOC_STRINGS = YES; 413 | SWIFT_VERSION = 5.0; 414 | }; 415 | name = Debug; 416 | }; 417 | 18801EC62CDDA161007AA6E8 /* Release */ = { 418 | isa = XCBuildConfiguration; 419 | buildSettings = { 420 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 421 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 422 | CODE_SIGN_ENTITLEMENTS = xHistory/xHistory.entitlements; 423 | CODE_SIGN_STYLE = Automatic; 424 | COMBINE_HIDPI_IMAGES = YES; 425 | CURRENT_PROJECT_VERSION = 19; 426 | DEAD_CODE_STRIPPING = YES; 427 | DEVELOPMENT_ASSET_PATHS = "\"xHistory/Preview Content\""; 428 | DEVELOPMENT_TEAM = L4T783637F; 429 | ENABLE_HARDENED_RUNTIME = YES; 430 | ENABLE_PREVIEWS = YES; 431 | GENERATE_INFOPLIST_FILE = YES; 432 | INFOPLIST_FILE = xHistory/Info.plist; 433 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; 434 | INFOPLIST_KEY_LSUIElement = YES; 435 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 lihaoyun6. All rights reserved."; 436 | LD_RUNPATH_SEARCH_PATHS = ( 437 | "$(inherited)", 438 | "@executable_path/../Frameworks", 439 | ); 440 | MACOSX_DEPLOYMENT_TARGET = 12.0; 441 | MARKETING_VERSION = 0.1.9; 442 | PRODUCT_BUNDLE_IDENTIFIER = com.lihaoyun6.xHistory; 443 | PRODUCT_NAME = "$(TARGET_NAME)"; 444 | SWIFT_EMIT_LOC_STRINGS = YES; 445 | SWIFT_VERSION = 5.0; 446 | }; 447 | name = Release; 448 | }; 449 | 18801F192CDDA38B007AA6E8 /* Debug */ = { 450 | isa = XCBuildConfiguration; 451 | buildSettings = { 452 | CODE_SIGN_STYLE = Automatic; 453 | DEAD_CODE_STRIPPING = YES; 454 | DEVELOPMENT_TEAM = L4T783637F; 455 | ENABLE_HARDENED_RUNTIME = YES; 456 | MACOSX_DEPLOYMENT_TARGET = 12.0; 457 | PRODUCT_NAME = "$(TARGET_NAME)"; 458 | SWIFT_VERSION = 5.0; 459 | }; 460 | name = Debug; 461 | }; 462 | 18801F1A2CDDA38B007AA6E8 /* Release */ = { 463 | isa = XCBuildConfiguration; 464 | buildSettings = { 465 | CODE_SIGN_STYLE = Automatic; 466 | DEAD_CODE_STRIPPING = YES; 467 | DEVELOPMENT_TEAM = L4T783637F; 468 | ENABLE_HARDENED_RUNTIME = YES; 469 | MACOSX_DEPLOYMENT_TARGET = 12.0; 470 | PRODUCT_NAME = "$(TARGET_NAME)"; 471 | SWIFT_VERSION = 5.0; 472 | }; 473 | name = Release; 474 | }; 475 | /* End XCBuildConfiguration section */ 476 | 477 | /* Begin XCConfigurationList section */ 478 | 18801EB02CDDA160007AA6E8 /* Build configuration list for PBXProject "xHistory" */ = { 479 | isa = XCConfigurationList; 480 | buildConfigurations = ( 481 | 18801EC22CDDA161007AA6E8 /* Debug */, 482 | 18801EC32CDDA161007AA6E8 /* Release */, 483 | ); 484 | defaultConfigurationIsVisible = 0; 485 | defaultConfigurationName = Release; 486 | }; 487 | 18801EC42CDDA161007AA6E8 /* Build configuration list for PBXNativeTarget "xHistory" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | 18801EC52CDDA161007AA6E8 /* Debug */, 491 | 18801EC62CDDA161007AA6E8 /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = Release; 495 | }; 496 | 18801F182CDDA38B007AA6E8 /* Build configuration list for PBXNativeTarget "xh" */ = { 497 | isa = XCConfigurationList; 498 | buildConfigurations = ( 499 | 18801F192CDDA38B007AA6E8 /* Debug */, 500 | 18801F1A2CDDA38B007AA6E8 /* Release */, 501 | ); 502 | defaultConfigurationIsVisible = 0; 503 | defaultConfigurationName = Release; 504 | }; 505 | /* End XCConfigurationList section */ 506 | 507 | /* Begin XCRemoteSwiftPackageReference section */ 508 | 18801EC92CDDA1EF007AA6E8 /* XCRemoteSwiftPackageReference "Sparkle" */ = { 509 | isa = XCRemoteSwiftPackageReference; 510 | repositoryURL = "https://github.com/sparkle-project/Sparkle"; 511 | requirement = { 512 | kind = upToNextMajorVersion; 513 | minimumVersion = 2.6.4; 514 | }; 515 | }; 516 | 18801ECC2CDDA1F9007AA6E8 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { 517 | isa = XCRemoteSwiftPackageReference; 518 | repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; 519 | requirement = { 520 | kind = upToNextMajorVersion; 521 | minimumVersion = 2.2.2; 522 | }; 523 | }; 524 | 18801ECF2CDDA216007AA6E8 /* XCRemoteSwiftPackageReference "SwiftTreeSitter" */ = { 525 | isa = XCRemoteSwiftPackageReference; 526 | repositoryURL = "https://github.com/ChimeHQ/SwiftTreeSitter.git"; 527 | requirement = { 528 | kind = upToNextMajorVersion; 529 | minimumVersion = 0.8.0; 530 | }; 531 | }; 532 | 18801ED42CDDA221007AA6E8 /* XCRemoteSwiftPackageReference "tree-sitter-bash" */ = { 533 | isa = XCRemoteSwiftPackageReference; 534 | repositoryURL = "https://github.com/tree-sitter/tree-sitter-bash.git"; 535 | requirement = { 536 | branch = master; 537 | kind = branch; 538 | }; 539 | }; 540 | 18801EDA2CDDA249007AA6E8 /* XCRemoteSwiftPackageReference "SFSMonitor" */ = { 541 | isa = XCRemoteSwiftPackageReference; 542 | repositoryURL = "https://github.com/ClassicalDude/SFSMonitor.git"; 543 | requirement = { 544 | branch = master; 545 | kind = branch; 546 | }; 547 | }; 548 | 18DEBA202CDF448B00A7EDED /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { 549 | isa = XCRemoteSwiftPackageReference; 550 | repositoryURL = "https://github.com/apple/swift-argument-parser.git"; 551 | requirement = { 552 | kind = upToNextMajorVersion; 553 | minimumVersion = 1.5.0; 554 | }; 555 | }; 556 | 18EC67342CDDDC5A00B5B1F9 /* XCRemoteSwiftPackageReference "MatrixColorSelector" */ = { 557 | isa = XCRemoteSwiftPackageReference; 558 | repositoryURL = "https://github.com/lihaoyun6/MatrixColorSelector.git"; 559 | requirement = { 560 | branch = main; 561 | kind = branch; 562 | }; 563 | }; 564 | /* End XCRemoteSwiftPackageReference section */ 565 | 566 | /* Begin XCSwiftPackageProductDependency section */ 567 | 18801ECA2CDDA1EF007AA6E8 /* Sparkle */ = { 568 | isa = XCSwiftPackageProductDependency; 569 | package = 18801EC92CDDA1EF007AA6E8 /* XCRemoteSwiftPackageReference "Sparkle" */; 570 | productName = Sparkle; 571 | }; 572 | 18801ECD2CDDA1F9007AA6E8 /* KeyboardShortcuts */ = { 573 | isa = XCSwiftPackageProductDependency; 574 | package = 18801ECC2CDDA1F9007AA6E8 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; 575 | productName = KeyboardShortcuts; 576 | }; 577 | 18801ED02CDDA216007AA6E8 /* SwiftTreeSitter */ = { 578 | isa = XCSwiftPackageProductDependency; 579 | package = 18801ECF2CDDA216007AA6E8 /* XCRemoteSwiftPackageReference "SwiftTreeSitter" */; 580 | productName = SwiftTreeSitter; 581 | }; 582 | 18801ED22CDDA216007AA6E8 /* SwiftTreeSitterLayer */ = { 583 | isa = XCSwiftPackageProductDependency; 584 | package = 18801ECF2CDDA216007AA6E8 /* XCRemoteSwiftPackageReference "SwiftTreeSitter" */; 585 | productName = SwiftTreeSitterLayer; 586 | }; 587 | 18801ED52CDDA221007AA6E8 /* TreeSitterBash */ = { 588 | isa = XCSwiftPackageProductDependency; 589 | package = 18801ED42CDDA221007AA6E8 /* XCRemoteSwiftPackageReference "tree-sitter-bash" */; 590 | productName = TreeSitterBash; 591 | }; 592 | 18801EDB2CDDA249007AA6E8 /* SFSMonitor */ = { 593 | isa = XCSwiftPackageProductDependency; 594 | package = 18801EDA2CDDA249007AA6E8 /* XCRemoteSwiftPackageReference "SFSMonitor" */; 595 | productName = SFSMonitor; 596 | }; 597 | 18DEBA212CDF448B00A7EDED /* ArgumentParser */ = { 598 | isa = XCSwiftPackageProductDependency; 599 | package = 18DEBA202CDF448B00A7EDED /* XCRemoteSwiftPackageReference "swift-argument-parser" */; 600 | productName = ArgumentParser; 601 | }; 602 | 18EC67352CDDDC5A00B5B1F9 /* MatrixColorSelector */ = { 603 | isa = XCSwiftPackageProductDependency; 604 | package = 18EC67342CDDDC5A00B5B1F9 /* XCRemoteSwiftPackageReference "MatrixColorSelector" */; 605 | productName = MatrixColorSelector; 606 | }; 607 | /* End XCSwiftPackageProductDependency section */ 608 | }; 609 | rootObject = 18801EAD2CDDA160007AA6E8 /* Project object */; 610 | } 611 | -------------------------------------------------------------------------------- /xHistory/ViewModel/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // xHistory 4 | // 5 | // Created by apple on 2024/11/6. 6 | // 7 | 8 | import SwiftUI 9 | import ServiceManagement 10 | import KeyboardShortcuts 11 | import MatrixColorSelector 12 | 13 | struct SettingsView: View { 14 | @State private var selectedItem: String? = "General" 15 | 16 | var body: some View { 17 | NavigationView { 18 | List(selection: $selectedItem) { 19 | NavigationLink(destination: GeneralView(), tag: "General", selection: $selectedItem) { 20 | Label("General", image: "gear") 21 | } 22 | NavigationLink(destination: HistoryView(), tag: "History", selection: $selectedItem) { 23 | Label("History", image: "history") 24 | } 25 | NavigationLink(destination: HotkeyView(), tag: "Hotkey", selection: $selectedItem) { 26 | Label("Hotkey", image: "hotkey") 27 | } 28 | NavigationLink(destination: ShellView(), tag: "Shell", selection: $selectedItem) { 29 | Label("Shell", image: "shell") 30 | } 31 | NavigationLink(destination: CloudView(), tag: "Cloud", selection: $selectedItem) { 32 | Label("Cloud", image: "cloud") 33 | } 34 | NavigationLink(destination: BlacklistView(), tag: "Blacklist", selection: $selectedItem) { 35 | Label("Blacklist", image: "block") 36 | } 37 | } 38 | .listStyle(.sidebar) 39 | .padding(.top, 9) 40 | } 41 | .frame(width: 600, height: 442) 42 | .navigationTitle("xHistory Settings") 43 | } 44 | } 45 | 46 | struct GeneralView: View { 47 | @AppStorage("panelOpacity") var panelOpacity = 100 48 | @AppStorage("statusBar") var statusBar = true 49 | //@AppStorage("dockIcon") var dockIcon = false 50 | @AppStorage("statusIconName") var statusIconName = "menuBar" 51 | 52 | @State private var showStatusBar = true 53 | @State private var launchAtLogin = false 54 | 55 | var body: some View { 56 | SForm { 57 | SGroupBox(label: "General") { 58 | if #available(macOS 13, *) { 59 | SToggle("Launch at Login", isOn: $launchAtLogin) 60 | .onAppear{ launchAtLogin = (SMAppService.mainApp.status == .enabled) } 61 | .onChange(of: launchAtLogin) { newValue in 62 | do { 63 | if newValue { 64 | try SMAppService.mainApp.register() 65 | } else { 66 | try SMAppService.mainApp.unregister() 67 | } 68 | }catch{ 69 | print("Failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)") 70 | } 71 | } 72 | SDivider() 73 | } 74 | SToggle("Show Menu bar Icon", isOn: $statusBar) 75 | SDivider() 76 | if showStatusBar { 77 | SItem(label: "Menu Bar Icon") { 78 | HStack { 79 | Button(action: { 80 | if let button = statusBarItem.button { 81 | statusIconName = "menuBarInvert" 82 | button.image = NSImage(named: statusIconName) 83 | } 84 | }, label: { 85 | ZStack { 86 | RoundedRectangle(cornerRadius: 5, style: .continuous) 87 | .foregroundStyle(statusIconName == "menuBarInvert" ? .blue : .clear) 88 | Image("menuBarInvert") 89 | .offset(x: 0.5, y: 0.5) 90 | .foregroundStyle(statusIconName == "menuBarInvert" ? .white : .secondary) 91 | }.frame(width: 24, height: 24) 92 | }).buttonStyle(.plain) 93 | Button(action: { 94 | if let button = statusBarItem.button { 95 | statusIconName = "menuBar" 96 | button.image = NSImage(named: statusIconName) 97 | } 98 | }, label: { 99 | ZStack { 100 | RoundedRectangle(cornerRadius: 5, style: .continuous) 101 | .foregroundStyle(statusIconName == "menuBar" ? .blue : .clear) 102 | Image("menuBar") 103 | .offset(x: 0.5, y: 0.5) 104 | .foregroundStyle(statusIconName == "menuBar" ? .white : .secondary) 105 | }.frame(width: 24, height: 24) 106 | }).buttonStyle(.plain) 107 | } 108 | } 109 | SDivider() 110 | } 111 | //SToggle("Show Dock Icon", isOn: $dockIcon) 112 | //SDivider() 113 | HStack { 114 | SSlider(label: "History Panel Opacity", value: $panelOpacity, range: 10...100, width: 160) 115 | Text("\(panelOpacity)%").frame(width: 35) 116 | } 117 | } 118 | SGroupBox(label: "Update") { UpdaterSettingsView(updater: updaterController.updater) } 119 | VStack(spacing: 8) { 120 | HStack { 121 | CheckForUpdatesView(updater: updaterController.updater) 122 | if !statusBar { 123 | Button(action: { 124 | NSApp.terminate(self) 125 | }, label: { 126 | Text("Quit".local + " xHistory").foregroundStyle(.red) 127 | }) 128 | } 129 | } 130 | if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { 131 | Text("xHistory v\(appVersion)") 132 | .font(.subheadline) 133 | .foregroundStyle(.secondary) 134 | } 135 | } 136 | } 137 | .onAppear { showStatusBar = statusBar } 138 | .onChange(of: statusBar) { newValue in 139 | showStatusBar = newValue 140 | statusBarItem.isVisible = newValue 141 | } 142 | /*.onChange(of: dockIcon) { newValue in 143 | if newValue { 144 | NSApp.setActivationPolicy(.regular) 145 | } else { 146 | NSApp.setActivationPolicy(.accessory) 147 | } 148 | }*/ 149 | } 150 | } 151 | 152 | struct HistoryView: View { 153 | @AppStorage("historyFile") var historyFile = "~/.bash_history" 154 | @AppStorage("autoClose") var autoClose = false 155 | @AppStorage("autoSpace") var autoSpace = false 156 | @AppStorage("noSameLine") var noSameLine = true 157 | @AppStorage("highlighting") var highlighting = true 158 | @AppStorage("autoReturn") var autoReturn = false 159 | 160 | @State private var userPath: String = "" 161 | @State private var disabled: Bool = false 162 | @State private var styleChanged: Bool = false 163 | @State private var functionColor: Color = ud.color(forKey: "functionColor") ?? Color(nsColor: .systemOrange) 164 | @State private var keywordColor: Color = ud.color(forKey: "keywordColor") ?? Color(nsColor: .systemPink) 165 | @State private var stringColor: Color = ud.color(forKey: "stringColor") ?? Color(nsColor: .systemGreen) 166 | @State private var propertyColor: Color = ud.color(forKey: "propertyColor") ?? Color(nsColor: .systemBlue) 167 | @State private var operatorColor: Color = ud.color(forKey: "operatorColor") ?? Color(nsColor: .systemGray) 168 | @State private var constantColor: Color = ud.color(forKey: "constantColor") ?? Color(nsColor: .systemMint) 169 | @State private var numberColor: Color = ud.color(forKey: "numberColor") ?? Color(nsColor: .red) 170 | @State private var embeddedColor: Color = ud.color(forKey: "embeddedColor") ?? Color(nsColor: .systemPurple) 171 | 172 | var body: some View { 173 | SForm { 174 | SGroupBox(label: "History") { 175 | SPicker("Read History From", selection: $historyFile) { 176 | Text("Zsh").tag("~/.zsh_history") 177 | Text("Bash").tag("~/.bash_history") 178 | if historyFile != "~/.bash_history" && historyFile != "~/.zsh_history" { 179 | Text("Custom").tag(historyFile) 180 | } else { 181 | Text("Custom").tag(historyFile.absolutePath) 182 | } 183 | }.onChange(of: historyFile) { newValue in 184 | HistoryCopyer.shared.updateHistory() 185 | for cmd in HistoryCopyer.shared.historys { 186 | SyntaxHighlighter.shared.getHighlightedTextAsync(for: cmd) { _ in } 187 | } 188 | queue?.removeAllURLs() 189 | DispatchQueue.main.async { 190 | Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { (timer) in 191 | if queue?.numberOfWatchedURLs() == 0 { 192 | timer.invalidate() 193 | _ = queue?.addURL(historyFile.absolutePath.url) 194 | } 195 | } 196 | } 197 | } 198 | SDivider() 199 | if historyFile != "~/.bash_history" && historyFile != "~/.zsh_history" { 200 | HStack { 201 | SField("Custom History File Path", text: $userPath) 202 | Button("Save") { historyFile = userPath } 203 | } 204 | SDivider() 205 | } 206 | SToggle("Merge adjacent duplicates", isOn: $noSameLine) 207 | SDivider() 208 | SToggle("Close history panel after filling", isOn: $autoClose) 209 | SDivider() 210 | SToggle("Auto-press Return after filling", isOn: $autoReturn) 211 | SDivider() 212 | SToggle("Add trailing space when filling", isOn: $autoSpace) 213 | } 214 | SGroupBox(label: "Highlight") { 215 | SToggle("Syntax Highlighting", isOn: $highlighting) 216 | SDivider() 217 | HStack { 218 | VStack(alignment: .leading, spacing: 0) { 219 | Text("Highlight Color Scheme") 220 | Text("Hover over a color to see more information and click to modify it.") 221 | .font(.footnote) 222 | .foregroundStyle(.secondary) 223 | } 224 | Spacer() 225 | Button(action: { 226 | functionColor = Color(nsColor: .systemOrange) 227 | keywordColor = Color(nsColor: .systemPink) 228 | stringColor = Color(nsColor: .systemGreen) 229 | propertyColor = Color(nsColor: .systemBlue) 230 | operatorColor = Color(nsColor: .systemGray) 231 | constantColor = Color(nsColor: .systemMint) 232 | numberColor = Color(nsColor: .red) 233 | embeddedColor = Color(nsColor: .systemPurple) 234 | }, label: { 235 | Image(systemName: "arrow.counterclockwise.circle.fill") 236 | .font(.system(size: 15, weight: .semibold)) 237 | .foregroundStyle(.secondary) 238 | }) 239 | .buttonStyle(.plain) 240 | .help("Reset Color Scheme") 241 | Button("Save") { HistoryCopyer.shared.reHighlight() } 242 | }.disabled(!highlighting) 243 | HStack { 244 | CS(tips: "The color of keywords in the code", name: "keywordColor", selection: $keywordColor, styleChanged: $styleChanged) 245 | CS(tips: "The color of functions in the code", name: "functionColor", selection: $functionColor, styleChanged: $styleChanged) 246 | CS(tips: "The color of constants in the code", name: "constantColor", selection: $constantColor, styleChanged: $styleChanged) 247 | CS(tips: "The color of properties in the code", name: "propertyColor", selection: $propertyColor, styleChanged: $styleChanged) 248 | CS(tips: "The color of operators in the code", name: "operatorColor", selection: $operatorColor, styleChanged: $styleChanged) 249 | CS(tips: "The color of strings in the code", name: "stringColor", selection: $stringColor, styleChanged: $styleChanged) 250 | CS(tips: "The color of embedded code in the code", name: "embeddedColor", selection: $embeddedColor, styleChanged: $styleChanged) 251 | CS(tips: "The color of numbers in the code", name: "numberColor", selection: $numberColor, styleChanged: $styleChanged) 252 | } 253 | .frame(height: 16) 254 | .padding(.bottom, 3) 255 | .disabled(disabled) 256 | } 257 | } 258 | .onAppear { userPath = historyFile } 259 | .onChange(of: historyFile) { newValue in userPath = newValue } 260 | .onChange(of: highlighting) { newValue in 261 | disabled = !newValue 262 | HistoryCopyer.shared.reHighlight() 263 | } 264 | } 265 | } 266 | 267 | struct HotkeyView: View { 268 | @State var hotKey1: String = "⌘← / ⌘→" 269 | @State var hotKey2: String = "⌃1" 270 | @State var hotKey3: String = "⌃2" 271 | @State var hotKey4: String = "⌃3" 272 | 273 | var body: some View { 274 | SForm(spacing: 10) { 275 | SGroupBox(label: "Hotkey") { 276 | SItem(label: "Open History Panel") { KeyboardShortcuts.Recorder("", name: .showPanel) } 277 | SDivider() 278 | SItem(label: "Open panel and show pinned history") { KeyboardShortcuts.Recorder("", name: .showPinnedPanel) } 279 | SDivider() 280 | SItem(label: "Open History Panel as Overlay") { 281 | HStack(spacing: -5) { 282 | SInfoButton(tips: "xHistory will detect the current frontmost window and open a floating panel of the same size on top of it.") 283 | KeyboardShortcuts.Recorder("", name: .showOverlay) 284 | } 285 | } 286 | SDivider() 287 | SItem(label: "Open overlay and show pinned history") { 288 | HStack(spacing: -5) { 289 | SInfoButton(tips: "xHistory will detect the current frontmost window and open a floating panel of the same size on top of it.") 290 | KeyboardShortcuts.Recorder("", name: .showPinnedOverlay) 291 | } 292 | } 293 | } 294 | SGroupBox { 295 | SItem(label: "Switch to \"History\" page") { 296 | TextField("", text: $hotKey2) 297 | .disabled(true) 298 | .frame(width: 128) 299 | .textFieldStyle(.roundedBorder) 300 | .multilineTextAlignment(.center) 301 | } 302 | SDivider() 303 | SItem(label: "Switch to \"Pinned\" page") { 304 | TextField("", text: $hotKey3) 305 | .disabled(true) 306 | .frame(width: 128) 307 | .textFieldStyle(.roundedBorder) 308 | .multilineTextAlignment(.center) 309 | } 310 | SDivider() 311 | SItem(label: "Switch to \"Archive\" page") { 312 | TextField("", text: $hotKey4) 313 | .disabled(true) 314 | .frame(width: 128) 315 | .textFieldStyle(.roundedBorder) 316 | .multilineTextAlignment(.center) 317 | } 318 | SDivider() 319 | SItem(label: "Swap action button positions") { 320 | HStack(spacing: 5) { 321 | SInfoButton(tips: "Put \"Copy\", \"Pin\" and \"Expand\" buttons on the other side of history items.\nThis is useful for full screen or very long window.") 322 | //KeyboardShortcuts.Recorder("", name: .swapButtons) 323 | TextField("", text: $hotKey1) 324 | .disabled(true) 325 | .frame(width: 128) 326 | .textFieldStyle(.roundedBorder) 327 | .multilineTextAlignment(.center) 328 | } 329 | } 330 | } 331 | } 332 | } 333 | } 334 | 335 | struct ShellView: View { 336 | @State private var cltInstalled: Bool = false 337 | @AppStorage("customShellConfig") var customShellConfig = true 338 | @AppStorage("historyLimit") var historyLimit = 1000 339 | @AppStorage("noDuplicates") var noDuplicates = true 340 | @AppStorage("realtimeSave") var realtimeSave = true 341 | @AppStorage("preFormatter") var preFormatter = "" 342 | @State private var disabled: Bool = false 343 | @State private var userFormatter = "" 344 | 345 | var body: some View { 346 | SForm(spacing: 10) { 347 | GroupBox(label: 348 | VStack(alignment: .leading) { 349 | Text("Shell Configuration").font(.headline) 350 | Text("These settings will only take effect in newly logged-in shells when you modify them.") 351 | .font(.footnote) 352 | .foregroundStyle(.secondary) 353 | } 354 | ) { 355 | VStack(spacing: 10) { 356 | SToggle("Custom Configuration (for Bash & Zsh)", isOn: $customShellConfig) 357 | SDivider() 358 | Group { 359 | SToggle("Real-time History Saving", isOn: $realtimeSave) 360 | SDivider() 361 | SToggle("Ignore Consecutive Duplicates", isOn: $noDuplicates) 362 | SDivider() 363 | SSteper("Maximum Number of History Items", value: $historyLimit, min: 1, max: 10000, width: 60) 364 | }.disabled(disabled) 365 | }.padding(5) 366 | } 367 | SGroupBox { 368 | HStack(spacing: 4) { 369 | SField("Preformatter", placeholder: "Enter regular expression here", text: $userFormatter) 370 | SInfoButton(tips: "You can use regular expressions to match each line of history.\nxHistory will only show you the content in the matching groups.") 371 | Button("Save") { preFormatter = userFormatter } 372 | } 373 | } 374 | SGroupBox { 375 | SButton("Command Line Tool", buttonTitle: cltInstalled ? "Uninstall" : "Install", 376 | tips: "After installation, you can run \"xhistory\" in yor terminal to quickly open the floating panel.") { 377 | if cltInstalled { 378 | CommandLineTool.uninstall { updateCTL() } 379 | } else { 380 | CommandLineTool.install { updateCTL() } 381 | } 382 | }.onAppear { cltInstalled = CommandLineTool.isInstalled() } 383 | } 384 | } 385 | .onAppear { userFormatter = preFormatter } 386 | .onChange(of: preFormatter) { _ in HistoryCopyer.shared.reHighlight() } 387 | .onChange(of: customShellConfig) { newValue in 388 | disabled = !newValue 389 | updateShellConfig() 390 | } 391 | .onChange(of: realtimeSave) { _ in updateShellConfig() } 392 | .onChange(of: noDuplicates) { _ in updateShellConfig() } 393 | .onChange(of: historyLimit) { _ in updateShellConfig() } 394 | } 395 | 396 | func updateCTL() { 397 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 398 | cltInstalled = CommandLineTool.isInstalled() 399 | } 400 | } 401 | } 402 | 403 | struct CloudView: View { 404 | @AppStorage("cloudSync") var cloudSync = false 405 | @AppStorage("cloudDirectory") var cloudDirectory = "" 406 | @StateObject private var state = PageState.shared 407 | 408 | var body: some View { 409 | SForm(spacing: 10, noSpacer: true) { 410 | SGroupBox(label: "Cloud") { 411 | SToggle("Cloud Archiving", isOn: $cloudSync) 412 | SDivider() 413 | SItem(label: "Archive Folder", spacing: 4) { 414 | Text(cloudDirectory) 415 | .font(.footnote) 416 | .foregroundColor(Color.secondary) 417 | .lineLimit(1) 418 | .truncationMode(.head) 419 | SInfoButton(tips: "Select a folder in iCloud Drive to store and sync history across multiple devices.") 420 | Button("Select...", action: { updateCloudDirectory() }) 421 | } 422 | } 423 | GroupBox(label: 424 | HStack(spacing: 5) { 425 | Text("Archives").font(.headline) 426 | Button(action: { 427 | state.archiveList = getCloudFiles() 428 | }, label: { 429 | Image(systemName: "arrow.triangle.2.circlepath") 430 | .font(.system(size: 10, weight: .bold)).foregroundStyle(.secondary) 431 | }).buttonStyle(.plain) 432 | }) { 433 | VStack(spacing: 10) { 434 | ScrollView(showsIndicators: true) { 435 | ForEach(state.archiveList.indices, id: \.self) { index in 436 | HStack { 437 | Text(state.archiveList[index]) 438 | Spacer() 439 | ConfirmButton(label: "Delete", title: "Delete This Archive?", confirmButton: "Delete") { 440 | if state.archiveList[index] == state.archiveName { 441 | state.archiveName = "" 442 | state.archiveData.removeAll() 443 | } 444 | let archiveURL = cloudDirectory.url.appendingPathComponent("\(state.archiveList[index]).xha") 445 | try? fd.removeItem(at: archiveURL) 446 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { state.archiveList = getCloudFiles() } 447 | } 448 | } 449 | .frame(height: 12) 450 | .padding(.vertical, 4) 451 | SDivider() 452 | } 453 | }.frame(maxWidth: .infinity) 454 | }.padding(5) 455 | } 456 | } 457 | .onAppear { state.archiveList = getCloudFiles() } 458 | .onChange(of: cloudSync) { _ in state.archiveList = getCloudFiles() } 459 | } 460 | 461 | func updateCloudDirectory() { 462 | let openPanel = NSOpenPanel() 463 | openPanel.canChooseFiles = false 464 | openPanel.canChooseDirectories = true 465 | openPanel.canCreateDirectories = true 466 | openPanel.allowedContentTypes = [] 467 | openPanel.allowsOtherFileTypes = false 468 | if openPanel.runModal() == NSApplication.ModalResponse.OK { 469 | if let path = openPanel.urls.first?.path { cloudDirectory = path } 470 | } 471 | } 472 | } 473 | 474 | struct ConfirmButton: View { 475 | var label: LocalizedStringKey 476 | var title: LocalizedStringKey = "Are you sure?" 477 | var confirmButton: LocalizedStringKey = "Confirm" 478 | var message: LocalizedStringKey = "You will not be able to recover it!" 479 | var action: () -> Void 480 | @State private var showAlert = false 481 | 482 | var body: some View { 483 | Button(action: { 484 | showAlert = true 485 | }, label: { 486 | Text(label).foregroundStyle(.red) 487 | }).alert(title, isPresented: $showAlert) { 488 | Button(confirmButton, role: .destructive) { action() } 489 | Button("Cancel", role: .cancel) {} 490 | } message: { 491 | Text(message) 492 | } 493 | } 494 | } 495 | 496 | struct BlacklistView: View { 497 | @State private var blockedItems = [String]() 498 | @State private var temp = "" 499 | @State private var showSheet = false 500 | @State private var editingIndex: Int? 501 | 502 | var body: some View { 503 | VStack { 504 | GroupBox(label: 505 | VStack(alignment: .leading) { 506 | Text("Blacklist").font(.headline) 507 | Text("The following commands will be ignored from the history") 508 | .font(.footnote) 509 | .foregroundStyle(.secondary) 510 | } 511 | ) { 512 | VStack(spacing: 10) { 513 | ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { 514 | List { 515 | ForEach(blockedItems.indices, id: \.self) { index in 516 | HStack { 517 | Image(systemName: "minus.circle.fill") 518 | .font(.system(size: 12)) 519 | .foregroundStyle(.red) 520 | .onTapGesture { if editingIndex == nil { blockedItems.remove(at: index) } } 521 | Text(blockedItems[index]) 522 | } 523 | } 524 | } 525 | Button(action: { 526 | showSheet = true 527 | }) { 528 | Image(systemName: "plus.square.fill") 529 | .font(.system(size: 20)) 530 | .foregroundStyle(.secondary) 531 | } 532 | .buttonStyle(.plain) 533 | .sheet(isPresented: $showSheet){ 534 | VStack { 535 | TextField("Enter Command".local, text: $temp).frame(width: 300) 536 | HStack(spacing: 20) { 537 | Button { 538 | if temp == "" { return } 539 | if !blockedItems.contains(temp) { blockedItems.append(temp) } 540 | temp = "" 541 | showSheet = false 542 | } label: { 543 | Text("Add to List").frame(width: 80) 544 | }.keyboardShortcut(.defaultAction) 545 | Button { 546 | showSheet = false 547 | } label: { 548 | Text("Cancel").frame(width: 80) 549 | } 550 | }.padding(.top, 10) 551 | }.padding() 552 | } 553 | } 554 | Text("You can add a \"#\" at the beginning of a keyword to convert it to a regex pattern") 555 | .font(.footnote) 556 | .foregroundStyle(.secondary) 557 | } 558 | .padding(5) 559 | .onAppear { blockedItems = (ud.object(forKey: "blockedCommands") as? [String]) ?? [] } 560 | .onChange(of: blockedItems) { 561 | b in ud.setValue(b, forKey: "blockedCommands") 562 | HistoryCopyer.shared.updateHistory() 563 | } 564 | } 565 | } 566 | .padding() 567 | .frame(maxWidth: .infinity) 568 | } 569 | } 570 | 571 | func getCloudFiles() -> [String] { 572 | @AppStorage("cloudDirectory") var cloudDirectory = "" 573 | var result = [String]() 574 | 575 | let contents = try? fd.contentsOfDirectory(atPath: cloudDirectory) 576 | result = contents?.filter { $0.hasSuffix(".\(cloudFileExtension)") }.map { $0.deletingPathExtension } ?? [] 577 | 578 | return result 579 | } 580 | 581 | struct CS: View { 582 | var tips: LocalizedStringKey 583 | var name: String 584 | @Binding var selection: Color 585 | @Binding var styleChanged: Bool 586 | 587 | var body: some View { 588 | HStack { 589 | if #unavailable(macOS 13) { 590 | ColorPicker("", selection: $selection).frame(width: 43) 591 | } else { 592 | MatrixColorSelector("", selection: $selection) 593 | } 594 | } 595 | .help(tips) 596 | .onChange(of: selection) { userColor in ud.setColor(userColor, forKey: name); styleChanged = true } 597 | } 598 | } 599 | 600 | struct FlowLayout: View { 601 | var items: [String] 602 | var spacing: CGFloat 603 | var content: (String) -> Content 604 | 605 | @State private var totalHeight = CGFloat.zero // Track total height of the layout 606 | 607 | var body: some View { 608 | VStack { 609 | GeometryReader { geometry in 610 | self.generateContent(in: geometry) 611 | } 612 | } 613 | .frame(height: totalHeight) // Set the total height dynamically 614 | } 615 | 616 | private func generateContent(in geometry: GeometryProxy) -> some View { 617 | var width = CGFloat.zero 618 | var height = CGFloat.zero 619 | var lastHeight = CGFloat.zero 620 | 621 | return ZStack(alignment: .topLeading) { 622 | ForEach(items.indices, id: \.self) { index in 623 | content(items[index]) 624 | .alignmentGuide(.leading) { dimension in 625 | if (abs(width - dimension.width) > geometry.size.width) { 626 | width = 0 // Move to the next line' 627 | height -= lastHeight + spacing 628 | } 629 | lastHeight = dimension.height // 记录当前元素的高度 630 | let result = width 631 | if items[index] == items.last! { // Last item 632 | width = 0 // Reset width 633 | } else { 634 | width -= dimension.width + spacing 635 | } 636 | return result 637 | } 638 | .alignmentGuide(.top) { _ in 639 | let result = height 640 | if items[index] == items.last! { // 最后一个元素,重置宽度和高度 641 | width = 0 642 | height = 0 643 | } 644 | return result 645 | } 646 | } 647 | }.background(viewHeightReader($totalHeight)) // Capture total height of the layout 648 | } 649 | 650 | private func viewHeightReader(_ binding: Binding) -> some View { 651 | GeometryReader { geometry -> Color in 652 | DispatchQueue.main.async { 653 | binding.wrappedValue = geometry.size.height 654 | } 655 | return Color.clear 656 | } 657 | } 658 | } 659 | 660 | extension KeyboardShortcuts.Name { 661 | static let showPanel = Self("showPanel") 662 | static let showPinnedPanel = Self("showPinnedPanel") 663 | static let showOverlay = Self("showOverlay") 664 | static let showPinnedOverlay = Self("showPinnedOverlay") 665 | //static let swapButtons = Self("switchButtons") 666 | } 667 | 668 | extension UserDefaults { 669 | func setColor(_ color: Color?, forKey key: String) { 670 | guard let color = color else { 671 | removeObject(forKey: key) 672 | return 673 | } 674 | 675 | do { 676 | let data = try NSKeyedArchiver.archivedData(withRootObject: NSColor(color), requiringSecureCoding: false) 677 | set(data, forKey: key) 678 | } catch { 679 | print("Error archiving color:", error) 680 | } 681 | } 682 | 683 | func color(forKey key: String) -> Color? { 684 | guard let data = data(forKey: key), 685 | let nsColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: data) else { 686 | return nil 687 | } 688 | return Color(nsColor) 689 | } 690 | 691 | func nsColor(forKey key: String) -> NSColor? { 692 | guard let data = data(forKey: key), 693 | let nsColor = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSColor.self, from: data) else { 694 | return nil 695 | } 696 | return nsColor 697 | } 698 | 699 | func cgColor(forKey key: String) -> CGColor? { return self.nsColor(forKey: key)?.cgColor } 700 | } 701 | -------------------------------------------------------------------------------- /xHistory/ViewModel/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // xHistory 4 | // 5 | // Created by apple on 2024/11/5. 6 | // 7 | 8 | import Carbon 9 | import SwiftUI 10 | 11 | class PageState: ObservableObject { 12 | static let shared = PageState() 13 | @Published var pageID: Int = 1 14 | @Published var archiveList: [String] = [] 15 | @Published var archiveName: String = "" 16 | @Published var archiveData: [String] = [] 17 | } 18 | 19 | struct SearchGroup: View { 20 | @Binding var keyWord: String 21 | @Binding var regexSearch: Bool 22 | @Binding var caseSensitivity: Bool 23 | 24 | var body: some View { 25 | HStack(spacing: 5) { 26 | Button(action: { 27 | regexSearch.toggle() 28 | }, label: { 29 | ZStack { 30 | RoundedRectangle(cornerRadius: 4, style: .continuous) 31 | .stroke(regexSearch ? .blue : .secondary.opacity(0.8), lineWidth: 1.5) 32 | .frame(width: 18, height: 18) 33 | HStack(alignment: .bottom, spacing: 1) { 34 | Image(systemName: "circle.fill") 35 | .font(.system(size: 2, weight: .medium)) 36 | Image(systemName: "asterisk") 37 | .font(.system(size: 7, weight: .black)) 38 | .padding(.bottom, 1) 39 | } 40 | .foregroundStyle(regexSearch ? .blue : .secondary.opacity(0.8)) 41 | .offset(x: 0.5, y: -1) 42 | } 43 | .frame(width: 18, height: 18) 44 | .background(Color.white.opacity(0.0001)) 45 | 46 | }) 47 | .buttonStyle(.plain) 48 | .focusable(false) 49 | .help("Regular expression") 50 | Button(action: { 51 | caseSensitivity.toggle() 52 | }, label: { 53 | ZStack { 54 | RoundedRectangle(cornerRadius: 4, style: .continuous) 55 | .stroke(caseSensitivity ? .blue : .secondary.opacity(0.8), lineWidth: 1.5) 56 | .frame(width: 18, height: 18) 57 | Image("textformat") 58 | .resizable() 59 | .scaledToFit() 60 | .frame(width: 14) 61 | .offset(y: -0.5) 62 | .foregroundStyle(caseSensitivity ? .blue : .secondary.opacity(0.8)) 63 | } 64 | .frame(width: 18, height: 18) 65 | .background(Color.white.opacity(0.001)) 66 | }) 67 | .buttonStyle(.plain) 68 | .focusable(false) 69 | .offset(x: 1) 70 | .help("Case sensitive") 71 | SearchField(search: $keyWord).frame(height: 21) 72 | }.frame(height: 16) 73 | } 74 | } 75 | 76 | struct ButtonGroup: View { 77 | var body: some View { 78 | HStack(spacing: 6) { 79 | HoverButton( 80 | color: .secondary.opacity(0.8), 81 | action: { 82 | mainPanel.close() 83 | openAboutPanel() 84 | }, 85 | label: { Image(systemName: "info.circle.fill") } 86 | ) 87 | HoverButton( 88 | color: .secondary.opacity(0.8), 89 | action: { openSettingPanel() }, 90 | label: { Image(systemName: "gearshape.fill") } 91 | ) 92 | }.focusable(false) 93 | } 94 | } 95 | 96 | struct ContentView: View { 97 | @AppStorage("historyFile") var historyFile = "~/.bash_history" 98 | @AppStorage("panelOpacity") var panelOpacity = 100 99 | //@AppStorage("showPinned") var showPinned = false 100 | @AppStorage("caseSensitivity") var caseSensitivity = false 101 | @AppStorage("regexSearch") var regexSearch = false 102 | @AppStorage("cloudSync") var cloudSync = false 103 | @AppStorage("cloudDirectory") var cloudDirectory = "" 104 | @AppStorage("buttonSide") var buttonSide = "right" 105 | 106 | @StateObject private var data = HistoryCopyer.shared 107 | @StateObject private var state = PageState.shared 108 | 109 | //@State private var newPanel = false 110 | @State private var scrollToTop = false 111 | @State private var keyWord: String = "" 112 | //@State private var archive: String = "" 113 | //@State private var archiveData: [String] = [] 114 | //@State private var showPin = 0 115 | @State private var overQuit: Bool = false 116 | @State private var result: [String] = [] 117 | @State private var resultP: [String] = [] 118 | @State private var resultA: [String] = [] 119 | @State private var pinnedList = (ud.object(forKey: "pinnedList") ?? []) as! [String] 120 | //@State private var archiveList = getCloudFiles() 121 | 122 | var fromMenubar: Bool = false 123 | 124 | var body: some View { 125 | ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) { 126 | ZStack { 127 | Button("") { if buttonSide == "right" { buttonSide = "left" } } 128 | .keyboardShortcut(.leftArrow, modifiers: [.command]) 129 | Button("") { if buttonSide == "left" { buttonSide = "right" } } 130 | .keyboardShortcut(.rightArrow, modifiers: [.command]) 131 | }.opacity(0) 132 | if !fromMenubar { 133 | Color.clear 134 | .background(.thinMaterial) 135 | .environment(\.controlActiveState, .active) 136 | .opacity(Double(panelOpacity) / 100) 137 | } 138 | VStack { 139 | HStack(spacing: 5) { 140 | if fromMenubar { 141 | Button(action: { 142 | NSApp.terminate(self) 143 | }, label: { 144 | Text("Quit") 145 | .font(.system(size: 9, weight: .semibold)) 146 | .foregroundStyle(.white) 147 | .frame(width: 26, height: 20) 148 | .background( 149 | RoundedRectangle(cornerRadius: 4.5, style: .continuous) 150 | .fill(overQuit ? .buttonRed.opacity(0.8) : .buttonRed) 151 | ) 152 | }) 153 | .buttonStyle(.plain) 154 | .padding(.leading, 1) 155 | .focusable(false) 156 | .onHover { hovering in overQuit = hovering } 157 | Spacer() 158 | Picker("", selection: $state.pageID) { 159 | Text("History").tag(1).keyboardShortcut("1", modifiers: [.control]) 160 | Text("Pinned").tag(2).keyboardShortcut("2", modifiers: [.control]) 161 | if cloudSync { Text("Cloud").tag(3).keyboardShortcut("3", modifiers: [.control]) } 162 | } 163 | .pickerStyle(.segmented) 164 | .fixedSize() 165 | .focusable(false) 166 | Spacer() 167 | ButtonGroup().offset(y: -1) 168 | } else { 169 | Group { 170 | Picker("", selection: $state.pageID) { 171 | Text("History").tag(1).keyboardShortcut("1", modifiers: [.control]) 172 | Text("Pinned").tag(2).keyboardShortcut("2", modifiers: [.control]) 173 | if cloudSync { Text("Archive").tag(3).keyboardShortcut("3", modifiers: [.control]) } 174 | } 175 | .pickerStyle(.segmented) 176 | .fixedSize() 177 | .padding(.leading, -8) 178 | .focusable(false) 179 | SearchGroup(keyWord: $keyWord, regexSearch: $regexSearch, caseSensitivity: $caseSensitivity) 180 | .padding(.leading, 4) 181 | }.padding(.vertical, -3) 182 | } 183 | } 184 | if fromMenubar { 185 | SearchGroup(keyWord: $keyWord, regexSearch: $regexSearch, caseSensitivity: $caseSensitivity) 186 | .padding(.leading, 2) 187 | .padding(.top, 5) 188 | } 189 | if state.pageID == 1 { 190 | ScrollViewReader { proxy in 191 | ScrollView(showsIndicators:false) { 192 | LazyVStack(alignment: .leading, spacing: 6) { 193 | ForEach(keyWord == "" ? data.historys.indices : result.indices, id: \.self) { index in 194 | CommandView(index: index, 195 | command: keyWord == "" ? data.historys[index] : result[index], 196 | pinnedList: $pinnedList, fromMenubar: fromMenubar) 197 | .id(index) 198 | .padding(.horizontal, 1) 199 | .shadow(color: (panelOpacity != 100 && !fromMenubar) ? .clear :.secondary.opacity(0.8), radius: 0.3, y: 0.5) 200 | } 201 | }.padding(.bottom, 1) 202 | } 203 | .focusable(false) 204 | .mask(RoundedRectangle(cornerRadius: 4, style: .continuous)) 205 | .onChange(of: scrollToTop) { _ in proxy.scrollTo(0, anchor: .top) } 206 | } 207 | } else if state.pageID == 2 { 208 | ScrollViewReader { proxy in 209 | ScrollView(showsIndicators:false) { 210 | LazyVStack(alignment: .leading, spacing: 6) { 211 | ForEach(keyWord == "" ? pinnedList.indices : resultP.indices, id: \.self) { index in 212 | CommandView(index: index, 213 | command: keyWord == "" ? pinnedList[index] : resultP[index], 214 | pinnedList: $pinnedList, fromMenubar: fromMenubar, editable: true) 215 | .id(index) 216 | .padding(.horizontal, 1) 217 | .shadow(color: (panelOpacity != 100 && !fromMenubar) ? .clear :.secondary.opacity(0.8), radius: 0.3, y: 0.5) 218 | } 219 | }.padding(.bottom, 1) 220 | } 221 | .focusable(false) 222 | .mask(RoundedRectangle(cornerRadius: 4, style: .continuous)) 223 | .onChange(of: scrollToTop) { _ in proxy.scrollTo(0, anchor: .top) } 224 | } 225 | } else { 226 | HStack(spacing: 5) { 227 | Picker(selection: $state.archiveName, content: { 228 | Text("Select an archive").tag("") 229 | ForEach(state.archiveList, id: \.self) { item in 230 | Text(item).tag(item) 231 | } 232 | }, label: {}) 233 | .onAppear { state.archiveList = getCloudFiles() } 234 | .onChange(of: state.archiveName) { newValue in 235 | if newValue != "" { 236 | let path = cloudDirectory.url.appendingPathComponent("\(newValue).xha").path 237 | state.archiveData = HistoryCopyer.shared.readHistory(file: path).reversed() 238 | } else { state.archiveData = [] } 239 | } 240 | Button("Refresh", action: { 241 | state.archiveList = getCloudFiles() 242 | if !state.archiveList.contains(state.archiveName) { 243 | state.archiveName = "" 244 | state.archiveData.removeAll() 245 | } else { 246 | let path = cloudDirectory.url.appendingPathComponent("\(state.archiveName).xha").path 247 | state.archiveData = HistoryCopyer.shared.readHistory(file: path).reversed() 248 | } 249 | }).keyboardShortcut("r", modifiers: [.command]) 250 | } 251 | .padding(.top, 3) 252 | .padding(.trailing, 1) 253 | ScrollViewReader { proxy in 254 | ScrollView(showsIndicators:false) { 255 | LazyVStack(alignment: .leading, spacing: 6) { 256 | ForEach(keyWord == "" ? state.archiveData.indices : resultA.indices, id: \.self) { index in 257 | CommandView(index: index, 258 | command: keyWord == "" ? state.archiveData[index] : resultA[index], 259 | pinnedList: $pinnedList, fromMenubar: fromMenubar) 260 | .id(index) 261 | .padding(.horizontal, 1) 262 | .shadow(color: (panelOpacity != 100 && !fromMenubar) ? .clear :.secondary.opacity(0.8), radius: 0.3, y: 0.5) 263 | } 264 | }.padding(.bottom, 1) 265 | } 266 | .focusable(false) 267 | .mask(RoundedRectangle(cornerRadius: 4, style: .continuous)) 268 | .onChange(of: scrollToTop) { _ in proxy.scrollTo(0, anchor: .top) } 269 | } 270 | } 271 | } 272 | .padding(7) 273 | .padding(.bottom, 1) 274 | .padding(.top, fromMenubar ? 0 : 21) 275 | if !fromMenubar { 276 | ButtonGroup() 277 | .padding(.horizontal, 7.5) 278 | .padding(.vertical, 6) 279 | } 280 | } 281 | .frame(minWidth: 360, minHeight: 119) 282 | .onAppear { data.historys = data.readHistory().reversed() } 283 | /*.overlay( 284 | RoundedRectangle(cornerRadius: 10, style: .continuous) 285 | .strokeBorder(.secondary.opacity(0.5), lineWidth: fromMenubar ? 0 : 1) 286 | )*/ 287 | .background( 288 | WindowAccessor(onWindowClose: { 289 | self.scrollToTop.toggle() 290 | self.keyWord = "" 291 | self.state.pageID = 1 292 | }) 293 | ) 294 | .onChange(of: keyWord) { newValue in updateResult() } 295 | .onChange(of: caseSensitivity) { _ in updateResult() } 296 | .onChange(of: regexSearch) { _ in updateResult() } 297 | .onChange(of: state.pageID) { _ in updateResult() } 298 | .onChange(of: data.historys) { _ in 299 | if state.pageID == 1 { result = searchHistory(data.historys) } 300 | } 301 | .onChange(of: pinnedList) { _ in 302 | if state.pageID == 2 { resultP = searchHistory(pinnedList) } 303 | } 304 | .onChange(of: state.archiveData) { _ in 305 | if state.pageID == 3 { resultA = searchHistory(state.archiveData) } 306 | } 307 | .onReceive(archiveTimer) { t in 308 | if data.needArchive { 309 | data.needArchive = false 310 | let archiveURL = cloudDirectory.url.appendingPathComponent("\(getMacDeviceName()) [\(historyFile.absolutePath.url.lastPathComponent.deletingPathExtension)].xha") 311 | if fd.fileExists(atPath: archiveURL.path) { try? fd.removeItem(at: archiveURL) } 312 | try? fd.copyItem(at: historyFile.absolutePath.url, to: archiveURL) 313 | } 314 | } 315 | .padding(.top, fromMenubar ? 0 : -28) 316 | } 317 | 318 | func updateResult() { 319 | if keyWord != "" { 320 | switch state.pageID { 321 | case 2: 322 | resultP = searchHistory(pinnedList) 323 | case 3: 324 | resultA = searchHistory(state.archiveData) 325 | default: 326 | result = searchHistory(data.historys) 327 | } 328 | } 329 | } 330 | 331 | func searchHistory(_ data: [String]) -> [String] { 332 | if !keyWord.isEmpty && !(keyWord == "") { 333 | if regexSearch { 334 | do { 335 | let options: NSRegularExpression.Options = caseSensitivity ? [] : [.caseInsensitive] 336 | let regex = try NSRegularExpression(pattern: keyWord, options: options) 337 | 338 | let matchingItems = data.filter { item in 339 | let range = NSRange(location: 0, length: item.utf16.count) 340 | return regex.firstMatch(in: item, options: [], range: range) != nil 341 | } 342 | return matchingItems 343 | } catch { 344 | //print("Invalid regular expression: \(error)") 345 | return [] 346 | } 347 | } else { 348 | let options: String.CompareOptions = caseSensitivity ? [] : [.caseInsensitive] 349 | return data.filter { $0.range(of: keyWord, options: options) != nil } 350 | } 351 | } 352 | return data 353 | } 354 | } 355 | 356 | struct ActionButtons: View { 357 | var command: String 358 | @State var cmd = "" 359 | @State var editable: Bool = false 360 | 361 | @Binding var showMore: Bool 362 | @Binding var pinnedList: [String] 363 | 364 | @State private var copied: Bool = false 365 | @State private var boomList = [String]() 366 | @State private var boomMode: Bool = false 367 | @State private var save: Bool = false 368 | 369 | var body: some View { 370 | HStack(spacing: 5) { 371 | HoverButton( 372 | color: .secondary, 373 | action: { 374 | copyToPasetboard(text: command) 375 | copied = true 376 | withAnimation(.easeInOut(duration: 1)) { copied = false } 377 | }, label: { 378 | Image(systemName: copied ? "checkmark.circle.fill" : "doc.on.clipboard") 379 | .font(.system(size: 12, weight: .medium)) 380 | .frame(width: 12) 381 | .frame(maxHeight: .infinity) 382 | }) 383 | HoverButton( 384 | color: .secondary, 385 | action: { 386 | if pinnedList.contains(command) { 387 | pinnedList.removeAll(where: { $0 == command }) 388 | } else { 389 | pinnedList.append(command) 390 | } 391 | ud.set(pinnedList, forKey: "pinnedList") 392 | }, label: { 393 | Image(systemName: pinnedList.contains(command) ? "pin.fill" : "pin") 394 | .font(.system(size: 13, weight: .bold)) 395 | .rotationEffect(.degrees(45)) 396 | .frame(width: 14) 397 | .frame(maxHeight: .infinity) 398 | }) 399 | HoverButton( 400 | color: .secondary, 401 | action: { 402 | if let regex = try? NSRegularExpression(pattern: #"(?:"[^"]*"|'[^']*'|`[^`]*`|[^;\s&|]+)"#) { 403 | let matches = regex.matches(in: command, range: NSRange(command.startIndex..., in: command)) 404 | boomList = matches.map { match in String(command[Range(match.range, in: command)!]) } 405 | boomMode = true 406 | } 407 | }, label: { 408 | Image(systemName: showMore ? "character.cursor.ibeam" : "rectangle.expand.vertical") 409 | .font(.system(size: 12, weight: .bold)) 410 | .frame(width: 14) 411 | .frame(maxHeight: .infinity) 412 | }).onHover { hovering in if hovering { showMore = true }} 413 | } 414 | .onAppear { cmd = command } 415 | .onChange(of: boomList) { _ in boomMode = true } 416 | .onChange(of: cmd) { _ in save = true } 417 | .sheet(isPresented: $boomMode) { 418 | VStack(spacing: 10) { 419 | GroupBox(label: Text("Magic Slice").font(.headline)) { 420 | FlowLayout(items: boomList, spacing: 6) { item in CommandSliceView(command: item) } 421 | } 422 | if editable { 423 | GroupBox(label: Text("Edit Command").font(.headline)) { 424 | ZStack { 425 | TextEditor(text: $cmd) 426 | .font(.system(size: 11, weight: .regular, design: .monospaced)) 427 | .multilineTextAlignment(.leading) 428 | .lineLimit(nil) 429 | .frame(maxWidth: .infinity, alignment: .leading) 430 | .textSelection(.enabled) 431 | .padding(3) 432 | }.background(Color.background) 433 | } 434 | } else { 435 | GroupBox(label: Text("Manual Selection").font(.headline)) { 436 | Text(AttributedString(SyntaxHighlighter.shared.getHighlightedText(for: command))) 437 | .font(.system(size: 11, weight: .regular, design: .monospaced)) 438 | .multilineTextAlignment(.leading) 439 | .lineLimit(nil) 440 | .frame(maxWidth: .infinity, alignment: .leading) 441 | .textSelection(.enabled) 442 | .padding(2) 443 | } 444 | } 445 | HStack { 446 | Spacer() 447 | if save { 448 | Button("Cancel") { boomMode = false } 449 | Button("Save") { 450 | if let index = pinnedList.firstIndex(of: command) { 451 | pinnedList[index] = command 452 | ud.set(pinnedList, forKey: "pinnedList") 453 | } 454 | boomMode = false 455 | }.keyboardShortcut(.defaultAction) 456 | } else { 457 | Button("Close") { boomMode = false } 458 | .keyboardShortcut(.defaultAction) 459 | } 460 | } 461 | } 462 | .padding() 463 | .focusable(false) 464 | } 465 | } 466 | } 467 | 468 | struct CommandView: View { 469 | var index: Int 470 | var command: String 471 | 472 | @Binding var pinnedList: [String] 473 | var fromMenubar: Bool = false 474 | var editable: Bool = false 475 | 476 | @AppStorage("panelOpacity") var panelOpacity = 100 477 | @AppStorage("autoClose") var autoClose = false 478 | @AppStorage("autoSpace") var autoSpace = false 479 | @AppStorage("autoReturn") var autoReturn = false 480 | @AppStorage("buttonSide") var buttonSide = "right" 481 | 482 | @State private var isHovered: Bool = false 483 | @State private var showMore: Bool = false 484 | 485 | var body: some View { 486 | HStack(spacing: 6) { 487 | Text((0...8).contains(index) ? "⌘\(index + 1)" : "\(index + 1)") 488 | .font(.system(size: 11, weight: (0...8).contains(index) ? .bold : .regular)) 489 | .foregroundStyle(isHovered ? .white : .primary) 490 | .padding(.horizontal, 2) 491 | .lineLimit(1) 492 | .minimumScaleFactor(0.3) 493 | .frame(width: 26) 494 | .frame(maxHeight: .infinity) 495 | .background(isHovered ? Color.blue : Color.background.opacity(fromMenubar ? 1 : Double(panelOpacity) / 100)) 496 | .mask(RoundedRectangle(cornerRadius: 5, style: .continuous)) 497 | .onHover { hovering in 498 | withAnimation(.easeInOut(duration: 0.1)) { 499 | isHovered = hovering 500 | } 501 | } 502 | HStack(spacing: 0) { 503 | if buttonSide == "left" { 504 | ActionButtons(command: command, editable: editable, showMore: $showMore, pinnedList: $pinnedList) 505 | .padding(.leading, 8) 506 | } 507 | Button(action: { 508 | copyToPasteboardAndPaste(text: "\(command)\(autoSpace ? " " : "")", enter: autoReturn) 509 | if autoClose { 510 | mainPanel.close() 511 | menuPopover.performClose(self) 512 | } 513 | }, label: { 514 | ZStack { 515 | Color.primary.opacity(0.0001) 516 | HStack { 517 | Text(AttributedString(SyntaxHighlighter.shared.getHighlightedText(for: command))) 518 | .font(.system(size: 11, weight: .regular, design: .monospaced)) 519 | .multilineTextAlignment(.leading) 520 | .lineLimit(showMore ? nil : 1) 521 | .padding(6) 522 | .padding(.leading, 2) 523 | Spacer() 524 | } 525 | } 526 | }) 527 | .buttonStyle(.plain) 528 | .setHotkey(index: index) 529 | if buttonSide == "right" { 530 | ActionButtons(command: command, editable: editable, showMore: $showMore, pinnedList: $pinnedList) 531 | .padding(.trailing, 8) 532 | } 533 | } 534 | .background(Color.background.opacity((isHovered || fromMenubar) ? 1.0 : Double(panelOpacity) / 100)) 535 | .frame(maxWidth: .infinity) 536 | .overlay { 537 | RoundedRectangle(cornerRadius: 4, style: .continuous) 538 | .stroke(.blue, lineWidth: isHovered ? 2 : 0) 539 | .padding(1) 540 | } 541 | .mask(RoundedRectangle(cornerRadius: 5, style: .continuous)) 542 | .onHover { hovering in 543 | isHovered = hovering 544 | if !hovering { showMore = false } 545 | } 546 | } 547 | } 548 | } 549 | 550 | struct CommandSliceView: View { 551 | var command: String 552 | @State private var isHovered: Bool = false 553 | @State private var copied: Bool = false 554 | 555 | @Environment(\.presentationMode) var presentationMode 556 | @AppStorage("autoClose") var autoClose = false 557 | @AppStorage("autoSpace") var autoSpace = false 558 | 559 | var body: some View { 560 | HStack(spacing: 0) { 561 | Button(action: { 562 | copyToPasteboardAndPaste(text: "\(command)\(autoSpace ? " " : "")") 563 | if autoClose { 564 | presentationMode.wrappedValue.dismiss() 565 | mainPanel.close() 566 | menuPopover.performClose(self) 567 | } 568 | }, label: { 569 | Text(command) 570 | .font(.system(size: 11, weight: .regular, design: .monospaced)) 571 | .lineLimit(1) 572 | .padding(.vertical, 6) 573 | .padding(.horizontal, 8) 574 | .padding(.trailing, 14) 575 | .help(command) 576 | }).buttonStyle(.plain) 577 | HoverButton( 578 | color: .secondary, 579 | action: { 580 | copyToPasetboard(text: command) 581 | copied = true 582 | withAnimation(.easeInOut(duration: 1)) { copied = false } 583 | }, label: { 584 | Image(systemName: copied ? "checkmark.circle.fill" : "doc.on.clipboard") 585 | .resizable().scaledToFit() 586 | .font(.system(size: 12, weight: .medium)) 587 | .frame(width: 12) 588 | }).padding(.leading, -18) 589 | } 590 | .background( 591 | RoundedRectangle(cornerRadius: 6, style: .continuous) 592 | .fill(.background2) 593 | .shadow(color: .secondary.opacity(0.8), radius: 0.3, y: 0.5) 594 | ) 595 | .overlay { 596 | RoundedRectangle(cornerRadius: 5, style: .continuous) 597 | .stroke(.blue, lineWidth: isHovered ? 2 : 0) 598 | .padding(1) 599 | } 600 | .onHover { hovering in isHovered = hovering } 601 | .focusable(false) 602 | } 603 | } 604 | 605 | struct ConditionalKeyboardShortcut: ViewModifier { 606 | var index: Int 607 | 608 | func body(content: Content) -> some View { 609 | if (0...8).contains(index) { 610 | content.keyboardShortcut( 611 | KeyEquivalent(Character("\(index + 1)")), 612 | modifiers: [.command] 613 | ) 614 | } else { 615 | content 616 | } 617 | } 618 | } 619 | 620 | extension View { 621 | func setHotkey(index: Int) -> some View { 622 | self.modifier(ConditionalKeyboardShortcut(index: index)) 623 | } 624 | } 625 | 626 | func copyToPasetboard(text: String) { 627 | let pasteboard = NSPasteboard.general 628 | pasteboard.clearContents() 629 | pasteboard.setString(text, forType: .string) 630 | } 631 | 632 | func copyToPasteboardAndPaste(text: String, enter: Bool = false) { 633 | let pasteboard = NSPasteboard.general 634 | var backupItems: [NSPasteboardItem] = [] 635 | 636 | for item in pasteboard.pasteboardItems ?? [] { 637 | let newItem = NSPasteboardItem() 638 | 639 | for type in item.types { 640 | if let data = item.data(forType: type) { 641 | newItem.setData(data, forType: type) 642 | } 643 | } 644 | backupItems.append(newItem) 645 | } 646 | 647 | pasteboard.clearContents() 648 | pasteboard.setString(text, forType: .string) 649 | 650 | let eventSource = CGEventSource(stateID: .hidSystemState) 651 | let cmdDown = CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(kVK_Command), keyDown: true) 652 | let cmdUp = CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(kVK_Command), keyDown: false) 653 | let vDown = CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(kVK_ANSI_V), keyDown: true) 654 | let vUp = CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(kVK_ANSI_V), keyDown: false) 655 | let enterDown = CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(kVK_Return), keyDown: true) 656 | let enterUp = CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(kVK_Return), keyDown: false) 657 | 658 | cmdDown?.flags = .maskCommand 659 | vDown?.flags = .maskCommand 660 | 661 | cmdDown?.post(tap: .cgAnnotatedSessionEventTap) 662 | vDown?.post(tap: .cgAnnotatedSessionEventTap) 663 | vUp?.post(tap: .cgAnnotatedSessionEventTap) 664 | cmdUp?.post(tap: .cgAnnotatedSessionEventTap) 665 | if enter { 666 | enterDown?.post(tap: .cgAnnotatedSessionEventTap) 667 | enterUp?.post(tap: .cgAnnotatedSessionEventTap) 668 | } 669 | 670 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 671 | pasteboard.clearContents() 672 | pasteboard.writeObjects(backupItems) 673 | } 674 | } 675 | 676 | struct SearchField: NSViewRepresentable { 677 | class Coordinator: NSObject, NSSearchFieldDelegate { 678 | var parent: SearchField 679 | 680 | init(_ parent: SearchField) { 681 | self.parent = parent 682 | } 683 | 684 | func controlTextDidChange(_ notification: Notification) { 685 | guard let searchField = notification.object as? NSSearchField else { 686 | print("Unexpected control in update notification") 687 | return 688 | } 689 | self.parent.search = searchField.stringValue 690 | } 691 | } 692 | 693 | @Binding var search: String 694 | @Environment(\.colorScheme) var colorScheme // 使用环境变量监听深浅色模式的变化 695 | 696 | func makeNSView(context: Context) -> NSSearchField { 697 | let searchField = NSSearchField(frame: .zero) 698 | searchField.translatesAutoresizingMaskIntoConstraints = false 699 | searchField.heightAnchor.constraint(equalToConstant: 24).isActive = true 700 | searchField.focusRingType = .none 701 | 702 | updateAppearance(for: searchField) // 设置初始 appearance 703 | return searchField 704 | } 705 | 706 | func updateNSView(_ searchField: NSSearchField, context: Context) { 707 | searchField.placeholderString = "Search".local 708 | searchField.stringValue = search 709 | searchField.delegate = context.coordinator 710 | 711 | // 每次更新时重新检查系统外观 712 | updateAppearance(for: searchField) 713 | } 714 | 715 | func makeCoordinator() -> Coordinator { 716 | return Coordinator(self) 717 | } 718 | 719 | private func updateAppearance(for searchField: NSSearchField) { 720 | if colorScheme == .dark { 721 | searchField.appearance = NSAppearance(named: .vibrantDark) 722 | } else { 723 | searchField.appearance = NSAppearance(named: .vibrantLight) 724 | } 725 | } 726 | } 727 | --------------------------------------------------------------------------------