You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
367 lines
8.9 KiB
Go
367 lines
8.9 KiB
Go
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 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(
|
|
"Sound enabled", "Whether sound is played", ProgramConfig.SoundEnabled,
|
|
)
|
|
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 {
|
|
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)
|
|
}
|