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/gen2brain/malgo" "github.com/getlantern/systray" ge "github.com/ronaldr1985/graceful-exit" "github.com/youpy/go-wav" "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"` WAVFile 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 PlayWAVFile(filename string, stopPlaying *bool) error { file, err := os.Open(filename) if err != nil { fmt.Println(err) os.Exit(1) } defer file.Close() w := wav.NewReader(file) f, err := w.Format() if err != nil { return err } reader := w channels := uint32(f.NumChannels) sampleRate := f.SampleRate ctx, err := malgo.InitContext(nil, malgo.ContextConfig{}, func(message string) { fmt.Printf("LOG <%v>\n", message) }) if err != nil { return err } defer func() { _ = ctx.Uninit() ctx.Free() }() deviceConfig := malgo.DefaultDeviceConfig(malgo.Playback) deviceConfig.Playback.Format = malgo.FormatS16 deviceConfig.Playback.Channels = channels deviceConfig.SampleRate = sampleRate deviceConfig.Alsa.NoMMap = 1 // This is the function that's used for sending more data to the device for playback. onSamples := func(pOutputSample, pInputSamples []byte, framecount uint32) { io.ReadFull(reader, pOutputSample) } deviceCallbacks := malgo.DeviceCallbacks{ Data: onSamples, } device, err := malgo.InitDevice(ctx.Context, deviceConfig, deviceCallbacks) if err != nil { return err } defer device.Uninit() err = device.Start() if err != nil { return err } for device.IsStarted() { if *stopPlaying { break } } return nil } func systrayOnReady() { var enabled bool = true var notificationsEnabled bool = true var soundEnabled bool = ProgramConfig.SoundEnabled var stopSound bool = false 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() 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( "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") mStopSound.Enable() PlayWAVFile(ProgramConfig.WAVFile, &stopSound) mStopSound.Disable() stopSound = false } 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 } case <-mStopSound.ClickedCh: stopSound = true } } }() } 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) }