├── 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 |
6 |
7 |
8 | ## 运行截图
9 |
10 |
11 |
12 |
13 |
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 |
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 |
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 |
--------------------------------------------------------------------------------