package main import ( "errors" "fmt" "io" "net/http" "os" "os/exec" "runtime" "strings" "time" darkmusicicon "gitea.ronald1985.uk/ronald1985/Eye-Reminder/icons/darkmusicicon" disabledicon "gitea.ronald1985.uk/ronald1985/Eye-Reminder/icons/disabledicon" enabledicon "gitea.ronald1985.uk/ronald1985/Eye-Reminder/icons/enabledicon" lightmusicicon "gitea.ronald1985.uk/ronald1985/Eye-Reminder/icons/lightmusicicon" "github.com/gen2brain/beeep" "github.com/getlantern/systray" ge "github.com/ronaldr1985/graceful-exit" "gopkg.in/yaml.v3" ) const ( DEFAULT_TIME_INBETWEEN_NOTIFICATIONS = 20 * time.Minute EYE_REMINDER_ICON_URL = "https://gitea.ronald1985.uk/ronald1985/Eye-Reminder/raw/branch/master/assets/Eye-Reminder-Icon.png" DEFAULT_CONFIG_FILE = "Interval: 20\nSoundEnabled: false\nWAVFile: \"\"\nDarkTheme: true" ) type Config struct { Interval int `yaml:"Interval"` SoundEnabled bool `yaml:"SoundEnabled"` WAVFile string `yaml:"WAVFile"` DarkTheme bool `yaml:"DarkTheme"` imageLocation string } var ( DEFAULT_UNIX_DIRECTORY = os.Getenv("HOME") + "/.config/Eye-Reminder/" DEFAULT_WINDOWS_DIRECTORY = os.Getenv("LOCALAPPDATA") + "/Eye-Reminder/" ProgramConfig Config ) func ReadConfig(filename string) (Config, error) { config := &Config{} bytes, err := os.ReadFile(filename) if err != nil { return *config, err } err = yaml.Unmarshal(bytes, config) if err != nil { return *config, fmt.Errorf("in file %q: %w", filename, err) } return *config, err } func CheckIfFileExists(filename string) (bool, error) { if _, err := os.Stat(filename); err == nil { return true, nil } else if errors.Is(err, os.ErrNotExist) { return false, nil } else { return false, err } } func DownloadFile(filename, url string) bool { out, err := os.Create(filename) if err != nil { return false } defer out.Close() resp, err := http.Get(url) if err != nil { out.Close() os.Remove(filename) return false } defer resp.Body.Close() _, err = io.Copy(out, resp.Body) return err == nil } func SendNotification(title string, message string) bool { path, err := exec.LookPath("herbe") if err == nil { var args []string args = append(args, title, message) cmd := exec.Command(path, args...) err := cmd.Start() if err != nil { return false } } else if strings.Contains(err.Error(), "executable file not found in ") { err := beeep.Notify(title, message, "assets/Eye-Reminder-Icon.png") if err != nil { return false } } return true } func systrayOnReady() { var enabled bool = true var notificationsEnabled bool = true var soundEnabled bool = ProgramConfig.SoundEnabled var stopSound bool = false currentIcon := enabledicon.Data systray.SetTitle("Eye Reminder") systray.SetTooltip("Eye Reminder") systray.SetIcon(enabledicon.Data) mEnabled := systray.AddMenuItemCheckbox( "Enabled", "Disables notifications and sound", true, ) systray.AddSeparator() mStopSound := systray.AddMenuItem( "Stop sound", "Stops the sound that is currently being played", ) mStopSound.Disable() mNotificationsEnabled := systray.AddMenuItemCheckbox( "Notifications enabled", "Whether notifications are enabled", true, ) mSoundEnabled := systray.AddMenuItemCheckbox( "Sound enabled", "Whether sound is played", ProgramConfig.SoundEnabled, ) systray.AddSeparator() mDarkThemeEnabled := systray.AddMenuItemCheckbox( "Dark mode enabled", "Whether the dark theme icons are used", ProgramConfig.DarkTheme, ) systray.AddSeparator() mQuitOrig := systray.AddMenuItem("Quit", "Quit the whole app") go func() { <-mQuitOrig.ClickedCh fmt.Println("Requesting quit") systray.Quit() fmt.Println("Finished quitting") }() go func() { lastTimeNotificationWasSent := time.Now().Add(time.Duration(-ProgramConfig.Interval) * time.Minute) for { if enabled && time.Since(lastTimeNotificationWasSent) > time.Duration(ProgramConfig.Interval)*time.Minute { if notificationsEnabled { fmt.Println("Sending notification") SendNotification("Eye Reminder", "Look away for the screen for atleast 20 seconds") } if soundEnabled { fmt.Println("Playing sound file") mStopSound.Enable() if ProgramConfig.DarkTheme { systray.SetIcon(lightmusicicon.Data) } else { systray.SetIcon(darkmusicicon.Data) } PlayWAVFile(ProgramConfig.WAVFile, &stopSound) mStopSound.Disable() stopSound = false systray.SetIcon(currentIcon) } lastTimeNotificationWasSent = time.Now() } } }() go func() { for { select { case <-mEnabled.ClickedCh: enabled = !enabled if mEnabled.Checked() { mEnabled.Uncheck() currentIcon = disabledicon.Data systray.SetIcon(currentIcon) } else { mEnabled.Check() currentIcon = enabledicon.Data systray.SetIcon(currentIcon) } case <-mNotificationsEnabled.ClickedCh: notificationsEnabled = !notificationsEnabled if mNotificationsEnabled.Checked() { mNotificationsEnabled.Uncheck() } else { mNotificationsEnabled.Check() } case <-mSoundEnabled.ClickedCh: soundEnabled = !soundEnabled if mSoundEnabled.Checked() { mSoundEnabled.Uncheck() } else { mSoundEnabled.Check() } case <-mStopSound.ClickedCh: stopSound = true case <-mDarkThemeEnabled.ClickedCh: ProgramConfig.DarkTheme = !ProgramConfig.DarkTheme if mDarkThemeEnabled.Checked() { mDarkThemeEnabled.Uncheck() } else { mDarkThemeEnabled.Check() } } } }() } func main() { ge.HandleSignals(false) ProgramConfig.Interval = int(DEFAULT_TIME_INBETWEEN_NOTIFICATIONS) var possibleAppDirectoryLocations []string possibleAppDirectoryLocations = append(possibleAppDirectoryLocations, os.Getenv("HOME")+"/.config/Eye-Reminder/") possibleAppDirectoryLocations = append(possibleAppDirectoryLocations, os.Getenv("HOME")+"/.config/Eye-Reminder/") possibleAppDirectoryLocations = append(possibleAppDirectoryLocations, os.Getenv("XDG_CONFIG_HOME")+"/Eye-Reminder/") possibleAppDirectoryLocations = append(possibleAppDirectoryLocations, os.Getenv("XDG_CONFIG_HOME")+"/Eye-Reminder/") possibleAppDirectoryLocations = append(possibleAppDirectoryLocations, os.Getenv("LOCALAPPDATA")+"/Eye-Reminder/") possibleAppDirectoryLocations = append(possibleAppDirectoryLocations, os.Getenv("LOCALAPPDATA")+"/Eye-Reminder/") appDirectory := "not found" for _, folder := range possibleAppDirectoryLocations { if _, err := os.Stat(folder); !os.IsNotExist(err) { appDirectory = folder break } } if appDirectory == "not found" { if runtime.GOOS == "windows" { appDirectory = DEFAULT_WINDOWS_DIRECTORY } else { appDirectory = DEFAULT_UNIX_DIRECTORY } fmt.Println("Creating config directory:", appDirectory) err := os.Mkdir(appDirectory, 0755) fmt.Println("Created folder:", appDirectory) if err != nil { panic(err) } } configFile := "" if fileExists, err := CheckIfFileExists(appDirectory + "config.yaml"); fileExists { configFile = appDirectory + "config.yaml" } else if !fileExists && err == nil { if fileExists, _ := CheckIfFileExists(appDirectory + "config.yml"); fileExists { configFile = appDirectory + "config.yml" } } if configFile != "" { fmt.Println("Found config file at", configFile) } if configFile == "" { configFile = appDirectory + "config.yaml" fmt.Println("Creating config file:", configFile) f, err := os.Create(configFile) if err != nil { panic("Failed to create config file") } fmt.Println("Created config file:", configFile) fmt.Println("Writing default config to", configFile) _, err = f.WriteString(DEFAULT_CONFIG_FILE) if err != nil { f.Close() panic("Failed to write to config file: " + configFile) } f.Close() fmt.Println("Written default config to ", configFile) } var err error ProgramConfig, err = ReadConfig(configFile) if err != nil { panic("Failed to read config file") } ProgramConfig.imageLocation = appDirectory + "Eye-Reminder-Icon.png" if fileExists, err := CheckIfFileExists(ProgramConfig.imageLocation); !fileExists && err == nil { fmt.Println("Downloading icon") DownloadFile(ProgramConfig.imageLocation, EYE_REMINDER_ICON_URL) fmt.Println("Downloaded icon") } else if err != nil { panic("Failed to check if " + ProgramConfig.imageLocation + " exists.") } systrayOnExit := func() { } systray.Run(systrayOnReady, systrayOnExit) }