// Copyright 2022 Arduino SA // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. //go:build !cli // Systray_real gets compiled when the tag 'cli' is missing. This is the default case package systray import ( "os" "os/user" log "github.com/sirupsen/logrus" "github.com/arduino/arduino-create-agent/icon" "github.com/arduino/go-paths-helper" "github.com/getlantern/systray" "github.com/go-ini/ini" "github.com/skratchdot/open-golang/open" ) // Start sets up the systray icon with its menus func (s *Systray) Start() { if s.Hibernate { systray.Run(s.startHibernate, s.end) } else { systray.Run(s.start, s.end) } } // Quit simply exits the program func (s *Systray) Quit() { systray.Quit() } // start creates a systray icon with menu options to go to arduino create, open debug, pause/quit the agent func (s *Systray) start() { systray.SetIcon(icon.GetIcon()) // Add version menuVer := systray.AddMenuItem("Agent version "+s.Version, "") menuVer.Disable() // Add links mURL := systray.AddMenuItem("Go to Arduino Create", "Arduino Create") mDebug := systray.AddMenuItem("Open Debug Console", "Debug console") mConfig := systray.AddMenuItem("Open Configuration", "Config File") // Remove crash-reports mRmCrashes := systray.AddMenuItem("Remove crash reports", "") s.updateMenuItem(mRmCrashes, s.CrashesIsEmpty()) // Add pause/quit mPause := systray.AddMenuItem("Pause Agent", "") systray.AddSeparator() mQuit := systray.AddMenuItem("Quit Agent", "") // Add configs s.addConfigs() // listen for events go func() { for { select { case <-mURL.ClickedCh: _ = open.Start("https://create.arduino.cc") case <-mDebug.ClickedCh: _ = open.Start(s.DebugURL()) case <-mConfig.ClickedCh: _ = open.Start(s.currentConfigFilePath.String()) case <-mRmCrashes.ClickedCh: s.RemoveCrashes() s.updateMenuItem(mRmCrashes, s.CrashesIsEmpty()) case <-mPause.ClickedCh: s.Pause() case <-mQuit.ClickedCh: s.Quit() } } }() } // updateMenuItem will enable or disable an item in the tray icon menu id disable is true func (s *Systray) updateMenuItem(item *systray.MenuItem, disable bool) { if disable { item.Disable() } else { item.Enable() } } // CrashesIsEmpty checks if the folder containing crash-reports is empty func (s *Systray) CrashesIsEmpty() bool { logsDir := getLogsDir() return logsDir.NotExist() // if the logs directory is empty we assume there are no crashreports } // RemoveCrashes removes the crash-reports from `logs` folder func (s *Systray) RemoveCrashes() { logsDir := getLogsDir() pathErr := logsDir.RemoveAll() if pathErr != nil { log.Errorf("Cannot remove crashreports: %s", pathErr) } else { log.Infof("Removed crashreports inside: %s", logsDir) } } // getLogsDir simply returns the folder containing the logs func getLogsDir() *paths.Path { usr, _ := user.Current() usrDir := paths.New(usr.HomeDir) // The user folder, on linux/macos /home/<usr>/ agentDir := usrDir.Join(".arduino-create") return agentDir.Join("logs") } // starthibernate creates a systray icon with menu options to resume/quit the agent func (s *Systray) startHibernate() { systray.SetIcon(icon.GetIconHiber()) mResume := systray.AddMenuItem("Resume Agent", "") systray.AddSeparator() mQuit := systray.AddMenuItem("Quit Agent", "") // listen for events go func() { for { select { case <-mResume.ClickedCh: s.Resume() case <-mQuit.ClickedCh: s.Quit() } } }() } // end simply exits the program func (s *Systray) end() { os.Exit(0) } func (s *Systray) addConfigs() { var mConfigCheckbox []*systray.MenuItem configs := s.getConfigs() if len(configs) > 1 { for _, config := range configs { entry := systray.AddMenuItem(config.Name, "") mConfigCheckbox = append(mConfigCheckbox, entry) // decorate configs gliph := " ☐ " if s.AdditionalConfig == config.Location { gliph = " 🗹 " } entry.SetTitle(gliph + config.Name) } } // It would be great to use the select channel here, // but unfortunately there's no clean way to do it with an array of channels, so we start a single goroutine for each of them for i := range mConfigCheckbox { go func(v int) { <-mConfigCheckbox[v].ClickedCh s.AdditionalConfig = configs[v].Location s.Restart() }(i) } } type configIni struct { Name string Location string } // getConfigs parses all config files in the .arduino-create folder func (s *Systray) getConfigs() []configIni { var configs []configIni files, err := s.ConfigDir.ReadDir() if err != nil { log.Errorf("cannot read the content of %s", s.ConfigDir) return nil } files.FilterOutDirs() files.FilterSuffix(".ini") for _, file := range files { cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true, AllowPythonMultilineValues: true}, file.String()) if err != nil { log.Errorf("error walking through executable configuration: %s", err) } else { defaultSection, err := cfg.GetSection("") name := defaultSection.Key("name").String() if name == "" || err != nil { name = "Default config" } conf := configIni{Name: name, Location: file.String()} configs = append(configs, conf) } } return configs }