Browse Source

refactor: streamline configuration management and enhance UI interactions

- Updated go.mod and go.sum to include necessary dependencies.
- Refactored README.md for clearer installation instructions and improved formatting.
- Enhanced main.go with better error handling and user feedback during execution.
- Improved configuration management in config.go, ensuring atomic writes and better error handling.
- Updated language support in lang.go for clearer user messages.
- Enhanced process management in manager.go to ensure more reliable process termination.
- Improved UI display methods for better user experience.
- Removed outdated test file generator_test.go to clean up the codebase.
- Updated install.ps1 script for better output formatting and error handling.
pull/85/head
Vaggelis kavouras 5 months ago
parent
commit
947d11fbc6
  1. 34
      README.md
  2. 308
      cmd/cursor-id-modifier/main.go
  3. 5
      go.mod
  4. 1
      go.sum
  5. 45
      internal/config/config.go
  6. 129
      internal/lang/lang.go
  7. 148
      internal/process/manager.go
  8. 105
      internal/ui/display.go
  9. 14
      internal/ui/logo.go
  10. 38
      internal/ui/spinner.go
  11. 106
      pkg/idgen/generator.go
  12. 26
      pkg/idgen/generator_test.go
  13. 48
      scripts/install.ps1

34
README.md

@ -58,21 +58,15 @@ this is a mistake.
### 🚀 One-Click Solution
<details>
<summary><b>Linux/macOS</b>: Copy and paste in terminal</summary>
**Linux/macOS**: Copy and paste in terminal
```bash
curl -fsSL https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.sh | sudo bash
```
</details>
<details>
<summary><b>Windows</b>: Copy and paste in PowerShell (Admin)</summary>
**Windows**: Copy and paste in PowerShell (Admin)
```powershell
irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex
```
</details>
That's it! The script will:
1. ✨ Install the tool automatically
@ -85,23 +79,23 @@ That's it! The script will:
<details>
<summary>Windows Packages</summary>
- 64-bit: `cursor-id-modifier_vX.X.X_Windows_x64.zip`
- 32-bit: `cursor-id-modifier_vX.X.X_Windows_x86.zip`
- 64-bit: `cursor-id-modifier_windows_x64.exe`
- 32-bit: `cursor-id-modifier_windows_x86.exe`
</details>
<details>
<summary>macOS Packages</summary>
- Intel: `cursor-id-modifier_vX.X.X_macOS_x64_intel.tar.gz`
- M1/M2: `cursor-id-modifier_vX.X.X_macOS_arm64_apple_silicon.tar.gz`
- Intel: `cursor-id-modifier_darwin_x64_intel`
- M1/M2: `cursor-id-modifier_darwin_arm64_apple_silicon`
</details>
<details>
<summary>Linux Packages</summary>
- 64-bit: `cursor-id-modifier_vX.X.X_Linux_x64.tar.gz`
- 32-bit: `cursor-id-modifier_vX.X.X_Linux_x86.tar.gz`
- ARM64: `cursor-id-modifier_vX.X.X_Linux_arm64.tar.gz`
- 64-bit: `cursor-id-modifier_linux_x64`
- 32-bit: `cursor-id-modifier_linux_x86`
- ARM64: `cursor-id-modifier_linux_arm64`
</details>
### 🔧 Technical Details
@ -176,21 +170,15 @@ this is a mistake.
### 🚀 一键解决
<details>
<summary><b>Linux/macOS</b>: 在终端中复制粘贴</summary>
**Linux/macOS**: 在终端中复制粘贴
```bash
curl -fsSL https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.sh | sudo bash
```
</details>
<details>
<summary><b>Windows</b>: 在PowerShell(管理员)中复制粘贴</summary>
**Windows**: 在PowerShell(管理员)中复制粘贴
```powershell
irm https://raw.githubusercontent.com/dacrab/go-cursor-help/master/scripts/install.ps1 | iex
```
</details>
就这样!脚本会:
1. ✨ 自动安装工具

308
cmd/cursor-id-modifier/main.go

@ -10,7 +10,6 @@ import (
"runtime"
"runtime/debug"
"strings"
"time"
"github.com/dacrab/go-cursor-help/internal/config"
"github.com/dacrab/go-cursor-help/internal/lang"
@ -21,6 +20,7 @@ import (
"github.com/sirupsen/logrus"
)
// Global variables
var (
version = "dev"
setReadOnly = flag.Bool("r", false, "set storage.json to read-only mode")
@ -29,7 +29,51 @@ var (
)
func main() {
// Initialize error recovery
setupErrorRecovery()
handleFlags()
setupLogger()
username := getCurrentUser()
log.Debug("Running as user:", username)
// Initialize components
display := ui.NewDisplay(nil)
configManager := initConfigManager(username)
generator := idgen.NewGenerator()
processManager := process.NewManager(nil, log)
// Check and handle privileges
if err := handlePrivileges(display); err != nil {
return
}
// Setup display
setupDisplay(display)
text := lang.GetText()
// Handle Cursor processes
if err := handleCursorProcesses(display, processManager); err != nil {
return
}
// Handle configuration
oldConfig := readExistingConfig(display, configManager, text)
newConfig := generateNewConfig(display, generator, oldConfig, text)
if err := saveConfiguration(display, configManager, newConfig); err != nil {
return
}
// Show completion messages
showCompletionMessages(display)
if os.Getenv("AUTOMATED_MODE") != "1" {
waitExit()
}
}
func setupErrorRecovery() {
defer func() {
if r := recover(); r != nil {
log.Errorf("Panic recovered: %v\n", r)
@ -37,230 +81,206 @@ func main() {
waitExit()
}
}()
}
// Parse flags
func handleFlags() {
flag.Parse()
// Show version if requested
if *showVersion {
fmt.Printf("Cursor ID Modifier v%s\n", version)
return
os.Exit(0)
}
}
// Initialize logger
func setupLogger() {
log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
FullTimestamp: true,
DisableLevelTruncation: true,
PadLevelText: true,
})
log.SetLevel(logrus.InfoLevel)
}
// Get current user
username := os.Getenv("SUDO_USER")
if username == "" {
user, err := user.Current()
if err != nil {
log.Fatal(err)
}
username = user.Username
func getCurrentUser() string {
if username := os.Getenv("SUDO_USER"); username != "" {
return username
}
// Initialize components
display := ui.NewDisplay(nil)
procManager := process.NewManager(process.DefaultConfig(), log)
user, err := user.Current()
if err != nil {
log.Fatal(err)
}
return user.Username
}
func initConfigManager(username string) *config.Manager {
configManager, err := config.NewManager(username)
if err != nil {
log.Fatal(err)
}
generator := idgen.NewGenerator()
return configManager
}
// Check privileges
func handlePrivileges(display *ui.Display) error {
isAdmin, err := checkAdminPrivileges()
if err != nil {
log.Error(err)
waitExit()
return
return err
}
if !isAdmin {
if runtime.GOOS == "windows" {
message := "\nRequesting administrator privileges..."
if lang.GetCurrentLanguage() == lang.CN {
message = "\n请求管理员权限..."
}
fmt.Println(message)
if err := selfElevate(); err != nil {
log.Error(err)
display.ShowPrivilegeError(
lang.GetText().PrivilegeError,
lang.GetText().RunAsAdmin,
lang.GetText().RunWithSudo,
lang.GetText().SudoExample,
)
waitExit()
return
}
return
return handleWindowsPrivileges(display)
}
display.ShowPrivilegeError(
lang.GetText().PrivilegeError,
lang.GetText().RunAsAdmin,
lang.GetText().RunWithSudo,
lang.GetText().SudoExample,
)
waitExit()
return
return fmt.Errorf("insufficient privileges")
}
return nil
}
// Ensure Cursor is closed
if err := ensureCursorClosed(display, procManager); err != nil {
message := "\nError: Please close Cursor manually before running this program."
if lang.GetCurrentLanguage() == lang.CN {
message = "\n错误:请在运行此程序之前手动关闭 Cursor。"
}
display.ShowError(message)
func handleWindowsPrivileges(display *ui.Display) error {
message := "\nRequesting administrator privileges..."
if lang.GetCurrentLanguage() == lang.CN {
message = "\n请求管理员权限..."
}
fmt.Println(message)
if err := selfElevate(); err != nil {
log.Error(err)
display.ShowPrivilegeError(
lang.GetText().PrivilegeError,
lang.GetText().RunAsAdmin,
lang.GetText().RunWithSudo,
lang.GetText().SudoExample,
)
waitExit()
return
return err
}
return nil
}
// Kill any remaining Cursor processes
if procManager.IsCursorRunning() {
text := lang.GetText()
display.ShowProcessStatus(text.ClosingProcesses)
if err := procManager.KillCursorProcesses(); err != nil {
fmt.Println()
message := "Warning: Could not close all Cursor instances. Please close them manually."
if lang.GetCurrentLanguage() == lang.CN {
message = "警告:无法关闭所有 Cursor 实例,请手动关闭。"
}
display.ShowWarning(message)
waitExit()
return
}
func setupDisplay(display *ui.Display) {
if err := display.ClearScreen(); err != nil {
log.Warn("Failed to clear screen:", err)
}
display.ShowLogo()
fmt.Println()
}
if procManager.IsCursorRunning() {
fmt.Println()
message := "\nWarning: Cursor is still running. Please close it manually."
if lang.GetCurrentLanguage() == lang.CN {
message = "\n警告:Cursor 仍在运行,请手动关闭。"
}
display.ShowWarning(message)
waitExit()
return
}
func handleCursorProcesses(display *ui.Display, processManager *process.Manager) error {
if os.Getenv("AUTOMATED_MODE") == "1" {
log.Debug("Running in automated mode, skipping Cursor process closing")
return nil
}
display.ShowProgress("Closing Cursor...")
log.Debug("Attempting to close Cursor processes")
display.ShowProcessStatus(text.ProcessesClosed)
fmt.Println()
if err := processManager.KillCursorProcesses(); err != nil {
log.Error("Failed to close Cursor:", err)
display.StopProgress()
display.ShowError("Failed to close Cursor. Please close it manually and try again.")
waitExit()
return err
}
// Clear screen
if err := display.ClearScreen(); err != nil {
log.Warn("Failed to clear screen:", err)
if processManager.IsCursorRunning() {
log.Error("Cursor processes still detected after closing")
display.StopProgress()
display.ShowError("Failed to close Cursor completely. Please close it manually and try again.")
waitExit()
return fmt.Errorf("cursor still running")
}
// Show logo
display.ShowLogo()
log.Debug("Successfully closed all Cursor processes")
display.StopProgress()
fmt.Println()
return nil
}
// Read existing config
text := lang.GetText()
func readExistingConfig(display *ui.Display, configManager *config.Manager, text lang.TextResource) *config.StorageConfig {
fmt.Println()
display.ShowProgress(text.ReadingConfig)
oldConfig, err := configManager.ReadConfig()
if err != nil {
log.Warn("Failed to read existing config:", err)
oldConfig = nil
}
display.StopProgress()
fmt.Println()
return oldConfig
}
// Generate new IDs
func generateNewConfig(display *ui.Display, generator *idgen.Generator, oldConfig *config.StorageConfig, text lang.TextResource) *config.StorageConfig {
display.ShowProgress(text.GeneratingIds)
newConfig := &config.StorageConfig{}
machineID, err := generator.GenerateMachineID()
if err != nil {
if machineID, err := generator.GenerateMachineID(); err != nil {
log.Fatal("Failed to generate machine ID:", err)
} else {
newConfig.TelemetryMachineId = machineID
}
macMachineID, err := generator.GenerateMacMachineID()
if err != nil {
if macMachineID, err := generator.GenerateMacMachineID(); err != nil {
log.Fatal("Failed to generate MAC machine ID:", err)
} else {
newConfig.TelemetryMacMachineId = macMachineID
}
deviceID, err := generator.GenerateDeviceID()
if err != nil {
if deviceID, err := generator.GenerateDeviceID(); err != nil {
log.Fatal("Failed to generate device ID:", err)
}
// Create new config
newConfig := &config.StorageConfig{
TelemetryMachineId: machineID,
TelemetryMacMachineId: macMachineID,
TelemetryDevDeviceId: deviceID,
} else {
newConfig.TelemetryDevDeviceId = deviceID
}
if oldConfig != nil && oldConfig.TelemetrySqmId != "" {
newConfig.TelemetrySqmId = oldConfig.TelemetrySqmId
} else if sqmID, err := generator.GenerateMacMachineID(); err != nil {
log.Fatal("Failed to generate SQM ID:", err)
} else {
sqmID, err := generator.GenerateMacMachineID()
if err != nil {
log.Fatal("Failed to generate SQM ID:", err)
}
newConfig.TelemetrySqmId = sqmID
}
// Save config
display.StopProgress()
fmt.Println()
return newConfig
}
func saveConfiguration(display *ui.Display, configManager *config.Manager, newConfig *config.StorageConfig) error {
display.ShowProgress("Saving configuration...")
if err := configManager.SaveConfig(newConfig, *setReadOnly); err != nil {
log.Error(err)
waitExit()
return
return err
}
display.StopProgress()
fmt.Println()
return nil
}
func showCompletionMessages(display *ui.Display) {
display.ShowSuccess(lang.GetText().SuccessMessage, lang.GetText().RestartMessage)
fmt.Println()
// Show success
display.ShowSuccess(text.SuccessMessage, text.RestartMessage)
message := "\nOperation completed!"
message := "Operation completed!"
if lang.GetCurrentLanguage() == lang.CN {
message = "\n操作完成!"
message = "操作完成!"
}
display.ShowInfo(message)
if os.Getenv("AUTOMATED_MODE") != "1" {
waitExit()
}
fmt.Println()
}
func waitExit() {
if os.Getenv("AUTOMATED_MODE") == "1" {
return
}
fmt.Println(lang.GetText().PressEnterToExit)
fmt.Print(lang.GetText().PressEnterToExit)
os.Stdout.Sync()
bufio.NewReader(os.Stdin).ReadString('\n')
}
func ensureCursorClosed(display *ui.Display, procManager *process.Manager) error {
maxAttempts := 3
text := lang.GetText()
display.ShowProcessStatus(text.CheckingProcesses)
for attempt := 1; attempt <= maxAttempts; attempt++ {
if !procManager.IsCursorRunning() {
display.ShowProcessStatus(text.ProcessesClosed)
fmt.Println()
return nil
}
message := fmt.Sprintf("Please close Cursor before continuing. Attempt %d/%d\n%s",
attempt, maxAttempts, text.PleaseWait)
if lang.GetCurrentLanguage() == lang.CN {
message = fmt.Sprintf("请在继续之前关闭 Cursor。尝试 %d/%d\n%s",
attempt, maxAttempts, text.PleaseWait)
}
display.ShowProcessStatus(message)
time.Sleep(5 * time.Second)
}
return fmt.Errorf("cursor is still running")
}
func checkAdminPrivileges() (bool, error) {
switch runtime.GOOS {
case "windows":

5
go.mod

@ -5,14 +5,11 @@ go 1.21
require (
github.com/fatih/color v1.15.0
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.10.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/sys v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

1
go.sum

@ -20,7 +20,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

45
internal/config/config.go

@ -32,10 +32,7 @@ func NewManager(username string) (*Manager, error) {
if err != nil {
return nil, fmt.Errorf("failed to get config path: %w", err)
}
return &Manager{
configPath: configPath,
}, nil
return &Manager{configPath: configPath}, nil
}
// ReadConfig reads the existing configuration
@ -69,22 +66,23 @@ func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error {
return fmt.Errorf("failed to create config directory: %w", err)
}
// Set file permissions
if err := os.Chmod(m.configPath, 0666); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to set file permissions: %w", err)
// Prepare updated configuration
updatedConfig := m.prepareUpdatedConfig(config)
// Write configuration
if err := m.writeConfigFile(updatedConfig, readOnly); err != nil {
return err
}
// Read existing config to preserve other fields
var originalFile map[string]interface{}
originalFileContent, err := os.ReadFile(m.configPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to read original file: %w", err)
} else if err == nil {
if err := json.Unmarshal(originalFileContent, &originalFile); err != nil {
return fmt.Errorf("failed to parse original file: %w", err)
}
} else {
originalFile = make(map[string]interface{})
return nil
}
// prepareUpdatedConfig merges existing config with updates
func (m *Manager) prepareUpdatedConfig(config *StorageConfig) map[string]interface{} {
// Read existing config
originalFile := make(map[string]interface{})
if data, err := os.ReadFile(m.configPath); err == nil {
json.Unmarshal(data, &originalFile)
}
// Update fields
@ -95,15 +93,20 @@ func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error {
originalFile["lastModified"] = time.Now().UTC().Format(time.RFC3339)
originalFile["version"] = "1.0.1"
return originalFile
}
// writeConfigFile handles the atomic write of the config file
func (m *Manager) writeConfigFile(config map[string]interface{}, readOnly bool) error {
// Marshal with indentation
newFileContent, err := json.MarshalIndent(originalFile, "", " ")
content, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
// Write to temporary file
tmpPath := m.configPath + ".tmp"
if err := os.WriteFile(tmpPath, newFileContent, 0666); err != nil {
if err := os.WriteFile(tmpPath, content, 0666); err != nil {
return fmt.Errorf("failed to write temporary file: %w", err)
}
@ -126,8 +129,8 @@ func (m *Manager) SaveConfig(config *StorageConfig, readOnly bool) error {
// Sync directory
if dir, err := os.Open(filepath.Dir(m.configPath)); err == nil {
defer dir.Close()
dir.Sync()
dir.Close()
}
return nil

129
internal/lang/lang.go

@ -7,34 +7,43 @@ import (
"sync"
)
// Language represents a supported language
// Language represents a supported language code
type Language string
const (
// CN represents Chinese language
CN Language = "cn"
// EN represents English language
// EN represents English language
EN Language = "en"
)
// TextResource contains all translatable text resources
type TextResource struct {
SuccessMessage string
RestartMessage string
ReadingConfig string
GeneratingIds string
PressEnterToExit string
ErrorPrefix string
PrivilegeError string
// Success messages
SuccessMessage string
RestartMessage string
// Progress messages
ReadingConfig string
GeneratingIds string
CheckingProcesses string
ClosingProcesses string
ProcessesClosed string
PleaseWait string
// Error messages
ErrorPrefix string
PrivilegeError string
// Instructions
RunAsAdmin string
RunWithSudo string
SudoExample string
ConfigLocation string
CheckingProcesses string
ClosingProcesses string
ProcessesClosed string
PleaseWait string
PressEnterToExit string
SetReadOnlyMessage string
// Info messages
ConfigLocation string
}
var (
@ -68,28 +77,32 @@ func GetText() TextResource {
// detectLanguage detects the system language
func detectLanguage() Language {
// Check environment variables
for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} {
if lang := os.Getenv(envVar); lang != "" && strings.Contains(strings.ToLower(lang), "zh") {
return CN
}
// Check environment variables first
if isChineseEnvVar() {
return CN
}
// Check Windows language settings
// Then check OS-specific locale
if isWindows() {
if isWindowsChineseLocale() {
return CN
}
} else {
// Check Unix locale
if isUnixChineseLocale() {
return CN
}
} else if isUnixChineseLocale() {
return CN
}
return EN
}
func isChineseEnvVar() bool {
for _, envVar := range []string{"LANG", "LANGUAGE", "LC_ALL"} {
if lang := os.Getenv(envVar); lang != "" && strings.Contains(strings.ToLower(lang), "zh") {
return true
}
}
return false
}
func isWindows() bool {
return os.Getenv("OS") == "Windows_NT"
}
@ -118,39 +131,57 @@ func isUnixChineseLocale() bool {
// texts contains all translations
var texts = map[Language]TextResource{
CN: {
SuccessMessage: "[√] 配置文件已成功更新!",
RestartMessage: "[!] 请手动重启 Cursor 以使更新生效",
ReadingConfig: "正在读取配置文件...",
GeneratingIds: "正在生成新的标识符...",
PressEnterToExit: "按回车键退出程序...",
ErrorPrefix: "程序发生严重错误: %v",
PrivilegeError: "\n[!] 错误:需要管理员权限",
// Success messages
SuccessMessage: "[√] 配置文件已成功更新!",
RestartMessage: "[!] 请手动重启 Cursor 以使更新生效",
// Progress messages
ReadingConfig: "正在读取配置文件...",
GeneratingIds: "正在生成新的标识符...",
CheckingProcesses: "正在检查运行中的 Cursor 实例...",
ClosingProcesses: "正在关闭 Cursor 实例...",
ProcessesClosed: "所有 Cursor 实例已关闭",
PleaseWait: "请稍候...",
// Error messages
ErrorPrefix: "程序发生严重错误: %v",
PrivilegeError: "\n[!] 错误:需要管理员权限",
// Instructions
RunAsAdmin: "请右键点击程序,选择「以管理员身份运行」",
RunWithSudo: "请使用 sudo 命令运行此程序",
SudoExample: "示例: sudo %s",
ConfigLocation: "配置文件位置:",
CheckingProcesses: "正在检查运行中的 Cursor 实例...",
ClosingProcesses: "正在关闭 Cursor 实例...",
ProcessesClosed: "所有 Cursor 实例已关闭",
PleaseWait: "请稍候...",
PressEnterToExit: "按回车键退出程序...",
SetReadOnlyMessage: "设置 storage.json 为只读模式, 这将导致 workspace 记录信息丢失等问题",
// Info messages
ConfigLocation: "配置文件位置:",
},
EN: {
SuccessMessage: "[√] Configuration file updated successfully!",
RestartMessage: "[!] Please restart Cursor manually for changes to take effect",
ReadingConfig: "Reading configuration file...",
GeneratingIds: "Generating new identifiers...",
PressEnterToExit: "Press Enter to exit...",
ErrorPrefix: "Program encountered a serious error: %v",
PrivilegeError: "\n[!] Error: Administrator privileges required",
// Success messages
SuccessMessage: "[√] Configuration file updated successfully!",
RestartMessage: "[!] Please restart Cursor manually for changes to take effect",
// Progress messages
ReadingConfig: "Reading configuration file...",
GeneratingIds: "Generating new identifiers...",
CheckingProcesses: "Checking for running Cursor instances...",
ClosingProcesses: "Closing Cursor instances...",
ProcessesClosed: "All Cursor instances have been closed",
PleaseWait: "Please wait...",
// Error messages
ErrorPrefix: "Program encountered a serious error: %v",
PrivilegeError: "\n[!] Error: Administrator privileges required",
// Instructions
RunAsAdmin: "Please right-click and select 'Run as Administrator'",
RunWithSudo: "Please run this program with sudo",
SudoExample: "Example: sudo %s",
ConfigLocation: "Config file location:",
CheckingProcesses: "Checking for running Cursor instances...",
ClosingProcesses: "Closing Cursor instances...",
ProcessesClosed: "All Cursor instances have been closed",
PleaseWait: "Please wait...",
PressEnterToExit: "Press Enter to exit...",
SetReadOnlyMessage: "Set storage.json to read-only mode, which will cause issues such as lost workspace records",
// Info messages
ConfigLocation: "Config file location:",
},
}

148
internal/process/manager.go

@ -12,22 +12,24 @@ import (
// Config holds process manager configuration
type Config struct {
MaxAttempts int
RetryDelay time.Duration
ProcessPatterns []string
MaxAttempts int // Maximum number of attempts to kill processes
RetryDelay time.Duration // Delay between retry attempts
ProcessPatterns []string // Process names to look for
}
// DefaultConfig returns the default configuration
func DefaultConfig() *Config {
return &Config{
MaxAttempts: 3,
RetryDelay: time.Second,
RetryDelay: 2 * time.Second,
ProcessPatterns: []string{
"Cursor.exe", // Windows
"Cursor", // Linux/macOS binary
"cursor", // Linux/macOS process
"cursor-helper", // Helper process
"cursor-id-modifier", // Our tool
"Cursor.exe", // Windows executable
"Cursor ", // Linux/macOS executable with space
"cursor ", // Linux/macOS executable lowercase with space
"cursor", // Linux/macOS executable lowercase
"Cursor", // Linux/macOS executable
"*cursor*", // Any process containing cursor
"*Cursor*", // Any process containing Cursor
},
}
}
@ -38,7 +40,7 @@ type Manager struct {
log *logrus.Logger
}
// NewManager creates a new process manager
// NewManager creates a new process manager with optional config and logger
func NewManager(config *Config, log *logrus.Logger) *Manager {
if config == nil {
config = DefaultConfig()
@ -52,7 +54,7 @@ func NewManager(config *Config, log *logrus.Logger) *Manager {
}
}
// IsCursorRunning checks if any Cursor process is running
// IsCursorRunning checks if any Cursor process is currently running
func (m *Manager) IsCursorRunning() bool {
processes, err := m.getCursorProcesses()
if err != nil {
@ -62,7 +64,7 @@ func (m *Manager) IsCursorRunning() bool {
return len(processes) > 0
}
// KillCursorProcesses attempts to kill all Cursor processes
// KillCursorProcesses attempts to kill all running Cursor processes
func (m *Manager) KillCursorProcesses() error {
for attempt := 1; attempt <= m.config.MaxAttempts; attempt++ {
processes, err := m.getCursorProcesses()
@ -74,68 +76,116 @@ func (m *Manager) KillCursorProcesses() error {
return nil
}
for _, proc := range processes {
if err := m.killProcess(proc); err != nil {
m.log.Warnf("Failed to kill process %s: %v", proc, err)
// Try graceful shutdown first on Windows
if runtime.GOOS == "windows" {
for _, pid := range processes {
exec.Command("taskkill", "/PID", pid).Run()
time.Sleep(500 * time.Millisecond)
}
}
// Force kill remaining processes
remainingProcesses, _ := m.getCursorProcesses()
for _, pid := range remainingProcesses {
m.killProcess(pid)
}
time.Sleep(m.config.RetryDelay)
}
if m.IsCursorRunning() {
return fmt.Errorf("failed to kill all Cursor processes after %d attempts", m.config.MaxAttempts)
if processes, _ := m.getCursorProcesses(); len(processes) == 0 {
return nil
}
}
return nil
}
// getCursorProcesses returns PIDs of running Cursor processes
func (m *Manager) getCursorProcesses() ([]string, error) {
var cmd *exec.Cmd
var processes []string
cmd := m.getProcessListCommand()
if cmd == nil {
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to execute command: %w", err)
}
return m.parseProcessList(string(output)), nil
}
// getProcessListCommand returns the appropriate command to list processes based on OS
func (m *Manager) getProcessListCommand() *exec.Cmd {
switch runtime.GOOS {
case "windows":
cmd = exec.Command("tasklist", "/FO", "CSV", "/NH")
return exec.Command("tasklist", "/FO", "CSV", "/NH")
case "darwin":
cmd = exec.Command("ps", "-ax")
return exec.Command("ps", "-ax")
case "linux":
cmd = exec.Command("ps", "-A")
return exec.Command("ps", "-A")
default:
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
return nil
}
}
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to execute command: %w", err)
// parseProcessList extracts Cursor process PIDs from process list output
func (m *Manager) parseProcessList(output string) []string {
var processes []string
for _, line := range strings.Split(output, "\n") {
lowerLine := strings.ToLower(line)
if m.isOwnProcess(lowerLine) {
continue
}
if pid := m.findCursorProcess(line, lowerLine); pid != "" {
processes = append(processes, pid)
}
}
return processes
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
for _, pattern := range m.config.ProcessPatterns {
if strings.Contains(strings.ToLower(line), strings.ToLower(pattern)) {
// Extract PID based on OS
pid := m.extractPID(line)
if pid != "" {
processes = append(processes, pid)
}
}
// isOwnProcess checks if the process belongs to this application
func (m *Manager) isOwnProcess(line string) bool {
return strings.Contains(line, "cursor-id-modifier") ||
strings.Contains(line, "cursor-helper")
}
// findCursorProcess checks if a process line matches Cursor patterns and returns its PID
func (m *Manager) findCursorProcess(line, lowerLine string) string {
for _, pattern := range m.config.ProcessPatterns {
if m.matchPattern(lowerLine, strings.ToLower(pattern)) {
return m.extractPID(line)
}
}
return ""
}
return processes, nil
// matchPattern checks if a line matches a pattern, supporting wildcards
func (m *Manager) matchPattern(line, pattern string) bool {
switch {
case strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*"):
search := pattern[1 : len(pattern)-1]
return strings.Contains(line, search)
case strings.HasPrefix(pattern, "*"):
return strings.HasSuffix(line, pattern[1:])
case strings.HasSuffix(pattern, "*"):
return strings.HasPrefix(line, pattern[:len(pattern)-1])
default:
return line == pattern
}
}
// extractPID extracts process ID from a process list line based on OS format
func (m *Manager) extractPID(line string) string {
switch runtime.GOOS {
case "windows":
// Windows CSV format: "ImageName","PID",...
parts := strings.Split(line, ",")
if len(parts) >= 2 {
return strings.Trim(parts[1], "\"")
}
case "darwin", "linux":
// Unix format: PID TTY TIME CMD
parts := strings.Fields(line)
if len(parts) >= 1 {
return parts[0]
@ -144,17 +194,23 @@ func (m *Manager) extractPID(line string) string {
return ""
}
// killProcess forcefully terminates a process by PID
func (m *Manager) killProcess(pid string) error {
var cmd *exec.Cmd
cmd := m.getKillCommand(pid)
if cmd == nil {
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
return cmd.Run()
}
// getKillCommand returns the appropriate command to kill a process based on OS
func (m *Manager) getKillCommand(pid string) *exec.Cmd {
switch runtime.GOOS {
case "windows":
cmd = exec.Command("taskkill", "/F", "/PID", pid)
return exec.Command("taskkill", "/F", "/PID", pid)
case "darwin", "linux":
cmd = exec.Command("kill", "-9", pid)
return exec.Command("kill", "-9", pid)
default:
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
return nil
}
return cmd.Run()
}

105
internal/ui/display.go

@ -10,92 +10,85 @@ import (
"github.com/fatih/color"
)
// Display handles UI display operations
// Display handles UI operations for terminal output
type Display struct {
spinner *Spinner
}
// NewDisplay creates a new display handler
// NewDisplay creates a new display instance with an optional spinner
func NewDisplay(spinner *Spinner) *Display {
if spinner == nil {
spinner = NewSpinner(nil)
}
return &Display{
spinner: spinner,
}
return &Display{spinner: spinner}
}
// ShowProgress shows a progress message with spinner
func (d *Display) ShowProgress(message string) {
d.spinner.SetMessage(message)
d.spinner.Start()
}
// Terminal Operations
// StopProgress stops the progress spinner
func (d *Display) StopProgress() {
d.spinner.Stop()
}
// ClearScreen clears the terminal screen
// ClearScreen clears the terminal screen based on OS
func (d *Display) ClearScreen() error {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/c", "cls")
} else {
default:
cmd = exec.Command("clear")
}
cmd.Stdout = os.Stdout
return cmd.Run()
}
// ShowProcessStatus shows the current process status
func (d *Display) ShowProcessStatus(message string) {
fmt.Printf("\r%s", strings.Repeat(" ", 80)) // Clear line
fmt.Printf("\r%s", color.CyanString("⚡ "+message))
}
// ShowPrivilegeError shows the privilege error message
func (d *Display) ShowPrivilegeError(errorMsg, adminMsg, sudoMsg, sudoExample string) {
red := color.New(color.FgRed, color.Bold)
yellow := color.New(color.FgYellow)
// Progress Indicator
red.Println(errorMsg)
if runtime.GOOS == "windows" {
yellow.Println(adminMsg)
} else {
yellow.Printf("%s\n%s\n", sudoMsg, fmt.Sprintf(sudoExample, os.Args[0]))
}
// ShowProgress displays a progress message with a spinner
func (d *Display) ShowProgress(message string) {
d.spinner.SetMessage(message)
d.spinner.Start()
}
// ShowSuccess shows a success message
func (d *Display) ShowSuccess(successMsg, restartMsg string) {
green := color.New(color.FgGreen, color.Bold)
yellow := color.New(color.FgYellow, color.Bold)
green.Printf("\n%s\n", successMsg)
yellow.Printf("%s\n", restartMsg)
// StopProgress stops the progress spinner
func (d *Display) StopProgress() {
d.spinner.Stop()
}
// ShowError shows an error message
func (d *Display) ShowError(message string) {
red := color.New(color.FgRed, color.Bold)
red.Printf("\n%s\n", message)
}
// Message Display
// ShowWarning shows a warning message
func (d *Display) ShowWarning(message string) {
yellow := color.New(color.FgYellow, color.Bold)
yellow.Printf("\n%s\n", message)
// ShowSuccess displays success messages in green
func (d *Display) ShowSuccess(messages ...string) {
green := color.New(color.FgGreen)
for _, msg := range messages {
green.Println(msg)
}
}
// ShowInfo shows an info message
// ShowInfo displays an info message in cyan
func (d *Display) ShowInfo(message string) {
cyan := color.New(color.FgCyan)
cyan.Printf("\n%s\n", message)
cyan.Println(message)
}
// ShowError displays an error message in red
func (d *Display) ShowError(message string) {
red := color.New(color.FgRed)
red.Println(message)
}
// ShowPrompt shows a prompt message and waits for user input
func (d *Display) ShowPrompt(message string) {
fmt.Print(message)
os.Stdout.Sync()
// ShowPrivilegeError displays privilege error messages with instructions
func (d *Display) ShowPrivilegeError(messages ...string) {
red := color.New(color.FgRed, color.Bold)
yellow := color.New(color.FgYellow)
// Main error message
red.Println(messages[0])
fmt.Println()
// Additional instructions
for _, msg := range messages[1:] {
if strings.Contains(msg, "%s") {
exe, _ := os.Executable()
yellow.Printf(msg+"\n", exe)
} else {
yellow.Println(msg)
}
}
}

14
internal/ui/logo.go

@ -5,15 +5,15 @@ import (
)
const cyberpunkLogo = `
______ ______ ______
/ ____/_ __________ ___ _____/ __/ // / / /
/ / / / / / ___/ _ \/ __ \/ ___/ /_/ // /_/ /
/ /___/ /_/ / / / __/ /_/ (__ ) __/__ __/ /
\____/\__,_/_/ \___/\____/____/_/ /_/ /_/
`
// ShowLogo displays the cyberpunk-style logo
// ShowLogo displays the application logo
func (d *Display) ShowLogo() {
cyan := color.New(color.FgCyan, color.Bold)
cyan.Println(cyberpunkLogo)

38
internal/ui/spinner.go

@ -10,8 +10,8 @@ import (
// SpinnerConfig defines spinner configuration
type SpinnerConfig struct {
Frames []string
Delay time.Duration
Frames []string // Animation frames for the spinner
Delay time.Duration // Delay between frame updates
}
// DefaultSpinnerConfig returns the default spinner configuration
@ -43,6 +43,8 @@ func NewSpinner(config *SpinnerConfig) *Spinner {
}
}
// State management
// SetMessage sets the spinner message
func (s *Spinner) SetMessage(message string) {
s.mu.Lock()
@ -50,7 +52,16 @@ func (s *Spinner) SetMessage(message string) {
s.message = message
}
// Start starts the spinner animation
// IsActive returns whether the spinner is currently active
func (s *Spinner) IsActive() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.active
}
// Control methods
// Start begins the spinner animation
func (s *Spinner) Start() {
s.mu.Lock()
if s.active {
@ -63,7 +74,7 @@ func (s *Spinner) Start() {
go s.run()
}
// Stop stops the spinner animation
// Stop halts the spinner animation
func (s *Spinner) Stop() {
s.mu.Lock()
defer s.mu.Unlock()
@ -75,20 +86,21 @@ func (s *Spinner) Stop() {
s.active = false
close(s.stopCh)
s.stopCh = make(chan struct{})
fmt.Println()
fmt.Print("\r") // Clear the spinner line
}
// IsActive returns whether the spinner is currently active
func (s *Spinner) IsActive() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.active
}
// Internal methods
func (s *Spinner) run() {
ticker := time.NewTicker(s.config.Delay)
defer ticker.Stop()
cyan := color.New(color.FgCyan, color.Bold)
message := s.message
// Print initial state
fmt.Printf("\r %s %s", cyan.Sprint(s.config.Frames[0]), message)
for {
select {
case <-s.stopCh:
@ -100,11 +112,11 @@ func (s *Spinner) run() {
return
}
frame := s.config.Frames[s.current%len(s.config.Frames)]
message := s.message
s.current++
s.mu.RUnlock()
fmt.Printf("\r%s %s", color.CyanString(frame), message)
fmt.Printf("\r %s", cyan.Sprint(frame))
fmt.Printf("\033[%dG%s", 4, message) // Move cursor and print message
}
}
}

106
pkg/idgen/generator.go

@ -1,95 +1,59 @@
package idgen
import (
cryptorand "crypto/rand"
"crypto/sha256"
"crypto/rand"
"encoding/hex"
"fmt"
"math/big"
"sync"
"time"
)
// Generator handles the generation of various IDs
type Generator struct {
charsetMu sync.RWMutex
charset string
}
// Generator handles secure ID generation for machines and devices
type Generator struct{}
// NewGenerator creates a new ID generator with default settings
// NewGenerator creates a new ID generator
func NewGenerator() *Generator {
return &Generator{
charset: "0123456789ABCDEFGHJKLMNPQRSTVWXYZ",
}
return &Generator{}
}
// SetCharset allows customizing the character set used for ID generation
func (g *Generator) SetCharset(charset string) {
g.charsetMu.Lock()
defer g.charsetMu.Unlock()
g.charset = charset
}
// Helper methods
// -------------
// GenerateMachineID generates a new machine ID with the format auth0|user_XX[unique_id]
func (g *Generator) GenerateMachineID() (string, error) {
prefix := "auth0|user_"
// simulateWork adds a small delay to make progress visible
func (g *Generator) simulateWork() {
time.Sleep(800 * time.Millisecond)
}
// Generate random sequence number between 0-99
seqNum, err := cryptorand.Int(cryptorand.Reader, big.NewInt(100))
if err != nil {
return "", fmt.Errorf("failed to generate sequence number: %w", err)
// generateRandomHex generates a random hex string of specified length
func (g *Generator) generateRandomHex(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("failed to generate random bytes: %w", err)
}
sequence := fmt.Sprintf("%02d", seqNum.Int64())
return hex.EncodeToString(bytes), nil
}
uniqueID, err := g.generateUniqueID(23)
if err != nil {
return "", fmt.Errorf("failed to generate unique ID: %w", err)
}
// Public methods
// -------------
fullID := prefix + sequence + uniqueID
return hex.EncodeToString([]byte(fullID)), nil
// GenerateMachineID generates a new 32-byte machine ID
func (g *Generator) GenerateMachineID() (string, error) {
g.simulateWork()
return g.generateRandomHex(32)
}
// GenerateMacMachineID generates a new MAC machine ID using SHA-256
// GenerateMacMachineID generates a new 64-byte MAC machine ID
func (g *Generator) GenerateMacMachineID() (string, error) {
data := make([]byte, 32)
if _, err := cryptorand.Read(data); err != nil {
return "", fmt.Errorf("failed to generate random data: %w", err)
}
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:]), nil
g.simulateWork()
return g.generateRandomHex(64)
}
// GenerateDeviceID generates a new device ID in UUID v4 format
// GenerateDeviceID generates a new device ID in UUID format
func (g *Generator) GenerateDeviceID() (string, error) {
uuid := make([]byte, 16)
if _, err := cryptorand.Read(uuid); err != nil {
return "", fmt.Errorf("failed to generate UUID: %w", err)
}
// Set version (4) and variant (2) bits according to RFC 4122
uuid[6] = (uuid[6] & 0x0f) | 0x40
uuid[8] = (uuid[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x",
uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:16]), nil
}
// generateUniqueID generates a random string of specified length using the configured charset
func (g *Generator) generateUniqueID(length int) (string, error) {
g.charsetMu.RLock()
defer g.charsetMu.RUnlock()
result := make([]byte, length)
max := big.NewInt(int64(len(g.charset)))
for i := range result {
randNum, err := cryptorand.Int(cryptorand.Reader, max)
if err != nil {
return "", fmt.Errorf("failed to generate random number: %w", err)
}
result[i] = g.charset[randNum.Int64()]
g.simulateWork()
id, err := g.generateRandomHex(16)
if err != nil {
return "", err
}
return string(result), nil
return fmt.Sprintf("%s-%s-%s-%s-%s",
id[0:8], id[8:12], id[12:16], id[16:20], id[20:32]), nil
}

26
pkg/idgen/generator_test.go

@ -1,26 +0,0 @@
package idgen
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewGenerator(t *testing.T) {
gen := NewGenerator()
assert.NotNil(t, gen, "Generator should not be nil")
}
func TestGenerateMachineID(t *testing.T) {
gen := NewGenerator()
id, err := gen.GenerateMachineID()
assert.NoError(t, err, "Should not return an error")
assert.NotEmpty(t, id, "Generated machine ID should not be empty")
}
func TestGenerateDeviceID(t *testing.T) {
gen := NewGenerator()
id, err := gen.GenerateDeviceID()
assert.NoError(t, err, "Should not return an error")
assert.NotEmpty(t, id, "Generated device ID should not be empty")
}

48
scripts/install.ps1

@ -9,13 +9,6 @@ if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdenti
# Set TLS to 1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Colors for output
$Red = "`e[31m"
$Green = "`e[32m"
$Blue = "`e[36m"
$Yellow = "`e[33m"
$Reset = "`e[0m"
# Create temporary directory
$TmpDir = Join-Path $env:TEMP ([System.Guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $TmpDir | Out-Null
@ -29,7 +22,7 @@ function Cleanup {
# Error handler
trap {
Write-Host "${Red}Error: $_${Reset}"
Write-Host "Error: $_" -ForegroundColor Red
Cleanup
exit 1
}
@ -44,7 +37,7 @@ function Get-SystemArch {
}
# Download with progress
function Download-WithProgress {
function Get-FileWithProgress {
param (
[string]$Url,
[string]$OutputFile
@ -58,18 +51,18 @@ function Download-WithProgress {
return $true
}
catch {
Write-Host "${Red}Failed to download: $_${Reset}"
Write-Host "Failed to download: $_" -ForegroundColor Red
return $false
}
}
# Main installation function
function Install-CursorModifier {
Write-Host "${Blue}Starting installation...${Reset}"
Write-Host "Starting installation..." -ForegroundColor Cyan
# Detect architecture
$arch = Get-SystemArch
Write-Host "${Green}Detected architecture: $arch${Reset}"
Write-Host "Detected architecture: $arch" -ForegroundColor Green
# Set installation directory
$InstallDir = "$env:ProgramFiles\CursorModifier"
@ -80,28 +73,36 @@ function Install-CursorModifier {
# Get latest release
try {
$latestRelease = Invoke-RestMethod -Uri "https://api.github.com/repos/dacrab/go-cursor-help/releases/latest"
Write-Host "Found latest release: $($latestRelease.tag_name)" -ForegroundColor Cyan
# Updated binary name format to match actual assets
$binaryName = "cursor-id-modifier_windows_$arch.exe"
$downloadUrl = $latestRelease.assets | Where-Object { $_.name -eq $binaryName } | Select-Object -ExpandProperty browser_download_url
Write-Host "Looking for asset: $binaryName" -ForegroundColor Cyan
$asset = $latestRelease.assets | Where-Object { $_.name -eq $binaryName }
$downloadUrl = $asset.browser_download_url
if (!$downloadUrl) {
Write-Host "Available assets:" -ForegroundColor Yellow
$latestRelease.assets | ForEach-Object { Write-Host $_.name }
throw "Could not find download URL for $binaryName"
}
}
catch {
Write-Host "${Red}Failed to get latest release: $_${Reset}"
Write-Host "Failed to get latest release: $_" -ForegroundColor Red
exit 1
}
# Download binary
Write-Host "${Blue}Downloading latest release...${Reset}"
Write-Host "Downloading latest release from $downloadUrl..." -ForegroundColor Cyan
$binaryPath = Join-Path $TmpDir "cursor-id-modifier.exe"
if (!(Download-WithProgress -Url $downloadUrl -OutputFile $binaryPath)) {
if (!(Get-FileWithProgress -Url $downloadUrl -OutputFile $binaryPath)) {
exit 1
}
# Install binary
Write-Host "${Blue}Installing...${Reset}"
Write-Host "Installing..." -ForegroundColor Cyan
try {
Copy-Item -Path $binaryPath -Destination "$InstallDir\cursor-id-modifier.exe" -Force
@ -112,24 +113,23 @@ function Install-CursorModifier {
}
}
catch {
Write-Host "${Red}Failed to install: $_${Reset}"
Write-Host "Failed to install: $_" -ForegroundColor Red
exit 1
}
Write-Host "${Green}Installation completed successfully!${Reset}"
Write-Host "${Blue}Running cursor-id-modifier...${Reset}"
Write-Host "Installation completed successfully!" -ForegroundColor Green
Write-Host "Running cursor-id-modifier..." -ForegroundColor Cyan
# Run the program
try {
$env:AUTOMATED_MODE = "1"
& "$InstallDir\cursor-id-modifier.exe"
if ($LASTEXITCODE -ne 0) {
Write-Host "${Red}Failed to run cursor-id-modifier${Reset}"
Write-Host "Failed to run cursor-id-modifier" -ForegroundColor Red
exit 1
}
}
catch {
Write-Host "${Red}Failed to run cursor-id-modifier: $_${Reset}"
Write-Host "Failed to run cursor-id-modifier: $_" -ForegroundColor Red
exit 1
}
}
@ -140,4 +140,6 @@ try {
}
finally {
Cleanup
Write-Host "Press any key to continue..."
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')
}
Loading…
Cancel
Save