package commands import ( "os" "os/exec" "strconv" "strings" "time" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" "github.com/vleeuwenmenno/kcm/src/config" "github.com/vleeuwenmenno/kcm/src/models" "golang.design/x/clipboard" ) func NewWatchCmd(history *models.History) *cobra.Command { cmd := &cobra.Command{ Use: "watch", Short: "Watch clipboard and store history", Aliases: []string{"--watch"}, Run: func(cmd *cobra.Command, args []string) { noDaemon, _ := cmd.Flags().GetBool("no-daemon") if !noDaemon { // Safety net: check if kcm daemon is already running (ignore self) out, err := exec.Command("ps", "-C", "kcm", "-o", "pid=").Output() if err == nil { pids := strings.Fields(string(out)) myPid := os.Getpid() for _, pidStr := range pids { pid, err := strconv.Atoi(pidStr) if err == nil && pid != myPid { log.Warn().Msg("kcm daemon is already running. Exiting to avoid duplicate daemons.") os.Exit(1) } } } // Set log file path since we are daemonizing cfg, _ := config.LoadConfig() if cfg.Logging.Path != "" { file, err := os.OpenFile(cfg.Logging.Path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil { log.Fatal().AnErr("err", err).Msg("Failed to open log file") } multi := zerolog.MultiLevelWriter(file, os.Stderr) log.Logger = log.Output(multi) } // Daemonize: fork and detach execPath, err := os.Executable() if err != nil { log.Fatal().Err(err).Msg("Failed to get executable path for daemonizing") } if os.Getppid() != 1 { attr := &os.ProcAttr{ Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, Env: os.Environ(), } proc, err := os.StartProcess(execPath, os.Args, attr) if err != nil { log.Fatal().Err(err).Msg("Failed to daemonize") } log.Info(). Str("logging_path", cfg.Logging.Path). Msgf("Daemon started with PID %d", proc.Pid) os.Exit(0) } } if err := clipboard.Init(); err != nil { log.Fatal().Err(err).Msg("Failed to initialize clipboard") } var ( lastText string lastImage []byte ) for { time.Sleep(250 * time.Millisecond) textOut := clipboard.Read(clipboard.FmtText) imgOut := clipboard.Read(clipboard.FmtImage) if len(textOut) > 0 { text := string(textOut) if text != lastText { item := models.HistoryItem{ Data: []byte(text), DataType: 0, // 0 for text Timestamp: time.Now(), Pinned: false, } history.Add(item) lastText = text log.Info().Str("content", text).Msg("Text item added ") } } if len(imgOut) > 0 && (lastImage == nil || string(imgOut) != string(lastImage)) { item := models.HistoryItem{ Data: imgOut, DataType: 1, // 1 for image/png Timestamp: time.Now(), Pinned: false, } history.Add(item) lastImage = imgOut log.Info().Msg("Image item added") } } }, } cmd.Flags().Bool("no-daemon", false, "Run in foreground (do not daemonize)") return cmd }