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.
This commit is contained in:
Vaggelis kavouras
2024-12-28 23:52:24 +02:00
parent 56f09677ca
commit 947d11fbc6
13 changed files with 541 additions and 502 deletions

View File

@@ -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

View File

@@ -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:",
},
}

View File

@@ -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,34 +76,34 @@ 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)
}
}
time.Sleep(m.config.RetryDelay)
}
// Force kill remaining processes
remainingProcesses, _ := m.getCursorProcesses()
for _, pid := range remainingProcesses {
m.killProcess(pid)
}
if m.IsCursorRunning() {
return fmt.Errorf("failed to kill all Cursor processes after %d attempts", m.config.MaxAttempts)
time.Sleep(m.config.RetryDelay)
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
switch runtime.GOOS {
case "windows":
cmd = exec.Command("tasklist", "/FO", "CSV", "/NH")
case "darwin":
cmd = exec.Command("ps", "-ax")
case "linux":
cmd = exec.Command("ps", "-A")
default:
cmd := m.getProcessListCommand()
if cmd == nil {
return nil, fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
@@ -110,32 +112,80 @@ func (m *Manager) getCursorProcesses() ([]string, error) {
return nil, fmt.Errorf("failed to execute command: %w", err)
}
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)
}
}
}
}
return processes, nil
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":
return exec.Command("tasklist", "/FO", "CSV", "/NH")
case "darwin":
return exec.Command("ps", "-ax")
case "linux":
return exec.Command("ps", "-A")
default:
return nil
}
}
// 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
}
// 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 ""
}
// 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
switch runtime.GOOS {
case "windows":
cmd = exec.Command("taskkill", "/F", "/PID", pid)
case "darwin", "linux":
cmd = exec.Command("kill", "-9", pid)
default:
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":
return exec.Command("taskkill", "/F", "/PID", pid)
case "darwin", "linux":
return exec.Command("kill", "-9", pid)
default:
return nil
}
}

View File

@@ -10,22 +10,37 @@ 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
// Terminal Operations
// ClearScreen clears the terminal screen based on OS
func (d *Display) ClearScreen() error {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("cmd", "/c", "cls")
default:
cmd = exec.Command("clear")
}
cmd.Stdout = os.Stdout
return cmd.Run()
}
// Progress Indicator
// ShowProgress displays a progress message with a spinner
func (d *Display) ShowProgress(message string) {
d.spinner.SetMessage(message)
d.spinner.Start()
@@ -36,66 +51,44 @@ func (d *Display) StopProgress() {
d.spinner.Stop()
}
// ClearScreen clears the terminal screen
func (d *Display) ClearScreen() error {
var cmd *exec.Cmd
if runtime.GOOS == "windows" {
cmd = exec.Command("cmd", "/c", "cls")
} else {
cmd = exec.Command("clear")
// Message Display
// ShowSuccess displays success messages in green
func (d *Display) ShowSuccess(messages ...string) {
green := color.New(color.FgGreen)
for _, msg := range messages {
green.Println(msg)
}
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))
// ShowInfo displays an info message in cyan
func (d *Display) ShowInfo(message string) {
cyan := color.New(color.FgCyan)
cyan.Println(message)
}
// ShowPrivilegeError shows the privilege error message
func (d *Display) ShowPrivilegeError(errorMsg, adminMsg, sudoMsg, sudoExample string) {
// ShowError displays an error message in red
func (d *Display) ShowError(message string) {
red := color.New(color.FgRed)
red.Println(message)
}
// 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)
red.Println(errorMsg)
if runtime.GOOS == "windows" {
yellow.Println(adminMsg)
} else {
yellow.Printf("%s\n%s\n", sudoMsg, fmt.Sprintf(sudoExample, os.Args[0]))
// 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)
}
}
}
// 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)
}
// ShowError shows an error message
func (d *Display) ShowError(message string) {
red := color.New(color.FgRed, color.Bold)
red.Printf("\n%s\n", message)
}
// ShowWarning shows a warning message
func (d *Display) ShowWarning(message string) {
yellow := color.New(color.FgYellow, color.Bold)
yellow.Printf("\n%s\n", message)
}
// ShowInfo shows an info message
func (d *Display) ShowInfo(message string) {
cyan := color.New(color.FgCyan)
cyan.Printf("\n%s\n", message)
}
// ShowPrompt shows a prompt message and waits for user input
func (d *Display) ShowPrompt(message string) {
fmt.Print(message)
os.Stdout.Sync()
}

View File

@@ -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)

View File

@@ -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
}
}
}