package main import ( "errors" "fmt" "io" "net/http" "os" "os/exec" "runtime" "strings" "time" disabledicon "gitea.ronald1985.uk/ronald1985/Eye-Reminder/icons/disabledicon" enabledicon "gitea.ronald1985.uk/ronald1985/Eye-Reminder/icons/enabledicon" "github.com/gen2brain/beeep" "github.com/getlantern/systray" "github.com/hajimehoshi/oto" ge "github.com/ronaldr1985/graceful-exit" "github.com/tosone/minimp3" "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\nMP3File: \"\"" ) type Config struct { Interval int `yaml:"Interval"` SoundEnabled bool `yaml:"SoundEnabled"` MP3File string `yaml:"MP3File"` imageLocation string } var ( DEFAULT_UNIX_DIRECTORY = os.Getenv("HOME") + "/.config/Eye-Reminder/" DEFAULT_WINDOWS_DIRECTORY = os.Getenv("LOCALAPPDATA") + "/Eye-Reminder/" ProgramConfig Config ) func print_fatal_error(err ...string) { fmt.Fprintln(os.Stderr, err) os.Exit(1) } 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) if err != nil { return false } return true } 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 PlayMP3File(file string) error { f, err := os.ReadFile(file) if err != nil { return err } decoder, data, err := minimp3.DecodeFull(f) if err != nil { return err } defer decoder.Close() var context *oto.Context if context, err = oto.NewContext(decoder.SampleRate, decoder.Channels, 2, 1024); err != nil { return err } player := context.NewPlayer() player.Write(data) if err = player.Close(); err != nil { return err } if err = context.Close(); err != nil { return err } return nil } func systrayOnReady() { var enabled bool = true var notificationsEnabled bool = true var soundEnabled bool = true systray.SetTitle("Eye Reminder") systray.SetTooltip("Eye Reminder") systray.SetIcon(enabledicon.Data) systray.AddSeparator() mEnabled := systray.AddMenuItemCheckbox( "Enabled", "Disables notifications and sound", true, ) systray.AddSeparator() mNotificationsEnabled := systray.AddMenuItemCheckbox( "Notifications enabled", "Whether notifications are enabled", true, ) mSoundEnabled := systray.AddMenuItemCheckbox( "Enabled", "Whether sound is played", true, ) 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") PlayMP3File(ProgramConfig.MP3File) } lastTimeNotificationWasSent = time.Now() } } }() go func() { for { select { case <-mEnabled.ClickedCh: if mEnabled.Checked() { mEnabled.Uncheck() enabled = false systray.SetIcon(disabledicon.Data) } else { mEnabled.Check() enabled = true systray.SetIcon(enabledicon.Data) } case <-mNotificationsEnabled.ClickedCh: if mNotificationsEnabled.Checked() { mNotificationsEnabled.Uncheck() notificationsEnabled = false } else { mNotificationsEnabled.Check() notificationsEnabled = true } case <-mSoundEnabled.ClickedCh: if mSoundEnabled.Checked() { mSoundEnabled.Uncheck() soundEnabled = false } else { mSoundEnabled.Check() soundEnabled = false } } } }() } 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 { print_fatal_error("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() print_fatal_error("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 { print_fatal_error("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 { print_fatal_error("Failed to check if ", ProgramConfig.imageLocation, " exists.") } systrayOnExit := func() { } systray.Run(systrayOnReady, systrayOnExit) }