feat: add stop command to terminate the clipboard watcher daemon

This commit is contained in:
Menno van Leeuwen 2025-05-21 00:07:48 +02:00
parent 2e77ee2333
commit 72705afc16
Signed by: vleeuwenmenno
SSH Key Fingerprint: SHA256:OJFmjANpakwD3F2Rsws4GLtbdz1TJ5tkQF0RZmF0TRE
5 changed files with 102 additions and 13 deletions

View File

@ -1,6 +1,8 @@
package commands
import (
"fmt"
"os"
"os/exec"
"strings"
@ -14,14 +16,26 @@ func NewStatusCmd() *cobra.Command {
Use: "status",
Short: "Check if the clipboard watcher daemon is running",
Run: func(cmd *cobra.Command, args []string) {
// Check for the process by name (simple approach)
out, err := exec.Command("ps", "-C", "kcm").Output()
out, err := exec.Command("ps", "-C", "kcm", "-o", "pid=").Output()
if err != nil {
log.Fatal().Err(err).Msg("Failed to check kcm daemon status")
}
lines := strings.Split(string(out), "\n")
if len(lines) > 1 {
log.Info().Msg("kcm daemon is running.")
pids := strings.Fields(string(out))
currentPID := os.Getpid()
filteredPIDs := make([]string, 0, len(pids))
for _, pidStr := range pids {
if pidStr == "" {
continue
}
if pidStr == fmt.Sprintf("%d", currentPID) {
continue
}
filteredPIDs = append(filteredPIDs, pidStr)
}
if len(filteredPIDs) > 0 {
log.Info().
Strs("pids", filteredPIDs).
Msg("kcm daemon is running.")
} else {
log.Warn().Msg("kcm daemon is not running, start a new one using `kcm watch`.")
}

54
src/commands/cmd_stop.go Normal file
View File

@ -0,0 +1,54 @@
package commands
import (
"os"
"os/exec"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
// NewStopCmd creates a new stop command to kill the kcm daemon.
func NewStopCmd() *cobra.Command {
return &cobra.Command{
Use: "stop",
Short: "Stop the clipboard watcher daemon",
Aliases: []string{"--stop", "kill"},
Run: func(cmd *cobra.Command, args []string) {
out, err := exec.Command("ps", "-C", "kcm", "-o", "pid=").Output()
if err != nil {
log.Fatal().Err(err).Msg("Failed to check for running kcm processes")
}
pids := strings.Fields(string(out))
if len(pids) == 0 {
log.Info().Msg("No running kcm daemon found.")
return
}
killed := 0
for _, pidStr := range pids {
pid, err := strconv.Atoi(pidStr)
if err != nil {
continue
}
if pid == os.Getpid() {
continue // Don't kill self
}
proc, err := os.FindProcess(pid)
if err == nil {
err = proc.Kill()
if err == nil {
log.Info().Msgf("Killed kcm daemon with PID %d", pid)
killed++
}
}
}
if killed == 0 {
log.Warn().Msg("No kcm daemons were killed.")
} else {
log.Info().Msgf("Stopped %d kcm daemon(s).", killed)
}
},
}
}

View File

@ -2,6 +2,9 @@ package commands
import (
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/rs/zerolog"
@ -20,9 +23,22 @@ func NewWatchCmd(history *models.History) *cobra.Command {
Run: func(cmd *cobra.Command, args []string) {
noDaemon, _ := cmd.Flags().GetBool("no-daemon")
if !noDaemon {
cfg, _ := config.LoadConfig()
// 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 {
@ -46,13 +62,13 @@ func NewWatchCmd(history *models.History) *cobra.Command {
if err != nil {
log.Fatal().Err(err).Msg("Failed to daemonize")
}
log.Info().Msgf("Daemon started with PID %d", proc.Pid)
log.Info().
Str("logging_path", cfg.Logging.Path).
Msgf("Daemon started with PID %d", proc.Pid)
os.Exit(0)
}
}
log.Info().Msg("Starting clipboard watcher (golang.design/x/clipboard)...")
if err := clipboard.Init(); err != nil {
log.Fatal().Err(err).Msg("Failed to initialize clipboard")
}
@ -63,7 +79,7 @@ func NewWatchCmd(history *models.History) *cobra.Command {
)
for {
time.Sleep(500 * time.Millisecond)
time.Sleep(250 * time.Millisecond)
textOut := clipboard.Read(clipboard.FmtText)
imgOut := clipboard.Read(clipboard.FmtImage)
@ -79,7 +95,7 @@ func NewWatchCmd(history *models.History) *cobra.Command {
}
history.Add(item)
lastText = text
log.Info().Str("content", text).Msg("Text clipboard item added to history")
log.Info().Str("content", text).Msg("Text item added ")
}
}
@ -92,7 +108,7 @@ func NewWatchCmd(history *models.History) *cobra.Command {
}
history.Add(item)
lastImage = imgOut
log.Info().Msg("Image clipboard item added to history")
log.Info().Msg("Image item added")
}
}
},

View File

@ -37,11 +37,15 @@ func LoadConfig() (Config, string) {
if err != nil {
continue
}
defer file.Close()
d := yaml.NewDecoder(file)
if err := d.Decode(&cfg); err == nil {
file.Close()
if cfg.Logging.Path == "" {
cfg.Logging.Path = logFilePath()
}
return cfg, path
}
file.Close()
}
cfg.Logging.Format = "console"

View File

@ -56,6 +56,7 @@ func main() {
commands.NewSearchCmd(history),
commands.NewStatusCmd(),
commands.NewVersionCmd(),
commands.NewStopCmd(), // Add the stop command
)
if err := rootCmd.Execute(); err != nil {