diff --git a/cli/commands/docker.go b/cli/commands/docker.go index bf9283d5..0dd6561f 100644 --- a/cli/commands/docker.go +++ b/cli/commands/docker.go @@ -11,7 +11,7 @@ var cmdDocker = &cobra.Command{ Aliases: []string{"dock"}, Short: "Docker command aliases", Run: func(cmd *cobra.Command, _ []string) { - cmd.Help() + _ = cmd.Help() }, } diff --git a/cli/commands/install.go b/cli/commands/install.go index 927afec6..c8b42123 100644 --- a/cli/commands/install.go +++ b/cli/commands/install.go @@ -10,45 +10,21 @@ import ( "github.com/spf13/cobra" ) -type component struct { - Name string - Description string - Alias string -} - -var components = []component{ - {"bin", "Installs ~/bin/* commands", ""}, - {"git", "Installs git extensions", ""}, - {"home", "Installs ~/.* config files", ""}, - {"zsh", "Installs zsh config files", ""}, - {"fonts", "Installs fonts", ""}, - {"homebrew", "Installs Homebrew dependencies", "brew"}, - {"npm", "Installs npm packages", ""}, - {"languages", "Installs asdf & languages", ""}, - {"vim", "Installs vim config", ""}, - {"hammerspoon", "Installs hammerspoon configuration", "hs"}, - {"osx", "Installs OSX configuration", ""}, - {"agents", "Installs agent skills (Claude Code + Codex)", ""}, -} - -func componentNames() []string { - names := make([]string, len(components)) - for i, c := range components { - names[i] = c.Name - } - return names -} - var cmdInstall = &cobra.Command{ Use: "install", Short: "Installs configuration", Run: func(cmd *cobra.Command, args []string) { if len(args) > 0 { - cmd.Help() + _ = cmd.Help() os.Exit(1) } - items := append([]string{"all"}, componentNames()...) + components := install.Components() + names := make([]string, len(components)) + for i, c := range components { + names[i] = c.Name + } + items := append([]string{"all"}, names...) prompt := promptui.Select{ Label: "Select component to install", @@ -79,7 +55,7 @@ func init() { }, ) - for _, c := range components { + for _, c := range install.Components() { var aliases []string if c.Alias != "" { aliases = []string{c.Alias} @@ -105,7 +81,7 @@ func installAll() { os.Exit(1) } - for _, name := range componentNames() { - install.Call(name) + for _, c := range install.Components() { + install.Call(c.Name) } } diff --git a/cli/commands/install/root.go b/cli/commands/install/root.go index 7cf68a7d..b83728e3 100644 --- a/cli/commands/install/root.go +++ b/cli/commands/install/root.go @@ -7,24 +7,40 @@ import ( "github.com/drn/dots/pkg/run" ) +// Component describes an installable component +type Component struct { + Name string + Description string + Alias string + Fn func() +} + +// Components returns the ordered list of installable components. +// This is the single source of truth for the component registry. +func Components() []Component { + return []Component{ + {"bin", "Installs ~/bin/* commands", "", Bin}, + {"git", "Installs git extensions", "", Git}, + {"home", "Installs ~/.* config files", "", Home}, + {"zsh", "Installs zsh config files", "", Zsh}, + {"fonts", "Installs fonts", "", Fonts}, + {"homebrew", "Installs Homebrew dependencies", "brew", Homebrew}, + {"npm", "Installs npm packages", "", Npm}, + {"languages", "Installs asdf & languages", "", Languages}, + {"vim", "Installs vim config", "", Vim}, + {"hammerspoon", "Installs hammerspoon configuration", "hs", Hammerspoon}, + {"osx", "Installs OSX configuration", "", Osx}, + {"agents", "Installs agent skills (Claude Code + Codex)", "", Agents}, + } +} + // Call - Call install command by name func Call(command string) { - installers := map[string]func(){ - "agents": Agents, - "bin": Bin, - "fonts": Fonts, - "git": Git, - "hammerspoon": Hammerspoon, - "home": Home, - "homebrew": Homebrew, - "languages": Languages, - "npm": Npm, - "osx": Osx, - "vim": Vim, - "zsh": Zsh, - } - if fn, ok := installers[command]; ok { - fn() + for _, c := range Components() { + if c.Name == command { + c.Fn() + return + } } } diff --git a/cli/commands/install/vim.go b/cli/commands/install/vim.go index b40b26eb..d5abd902 100644 --- a/cli/commands/install/vim.go +++ b/cli/commands/install/vim.go @@ -67,7 +67,7 @@ func vimUpdatePlug() { func vimUpdatePlugins() { log.Info("Updating vim plugins:") tempPath := "/tmp/vim-update-result" - os.Remove(tempPath) + _ = os.Remove(tempPath) run.Silent( "nvim -c \"%s\"", strings.Join( diff --git a/cli/commands/root.go b/cli/commands/root.go index c1535d4e..569cec42 100644 --- a/cli/commands/root.go +++ b/cli/commands/root.go @@ -12,7 +12,7 @@ var root = &cobra.Command{ Use: "dots", Short: "The dots CLI manages your development environment dependencies", Run: func(cmd *cobra.Command, _ []string) { - cmd.Help() + _ = cmd.Help() }, } diff --git a/cli/commands/spinner.go b/cli/commands/spinner.go index c1145cdb..2bc1075e 100644 --- a/cli/commands/spinner.go +++ b/cli/commands/spinner.go @@ -14,7 +14,7 @@ var cmdSpinner = &cobra.Command{ Use: "spinner", Short: "Runs simple CLI spinners", Run: func(cmd *cobra.Command, _ []string) { - cmd.Help() + _ = cmd.Help() }, } diff --git a/cli/config/root.go b/cli/config/root.go index e7856469..6f0a9280 100644 --- a/cli/config/root.go +++ b/cli/config/root.go @@ -132,7 +132,9 @@ func save(cfg *ini.File) { log.Warning("Failed to create config directory: %s", err.Error()) return } - cfg.SaveTo(configPath()) + if err := cfg.SaveTo(configPath()); err != nil { + log.Warning("Failed to save config: %s", err.Error()) + } } func config() *ini.File { diff --git a/cli/git/root.go b/cli/git/root.go index 86d3fbfc..697c5865 100644 --- a/cli/git/root.go +++ b/cli/git/root.go @@ -102,8 +102,8 @@ func Create(branch string) bool { } // ResetHard - Hard resets to the specified address -func ResetHard(address string) { - run.Verbose("git reset --hard %s", address) +func ResetHard(address string) bool { + return run.Verbose("git reset --hard %s", address) == nil } // Delete - Deletes the specified branch diff --git a/cli/link/root.go b/cli/link/root.go index f4ccae3f..7244e0e1 100644 --- a/cli/link/root.go +++ b/cli/link/root.go @@ -10,30 +10,18 @@ import ( // Soft - Creates a soft link between input from and to arguments func Soft(from string, to string) { - log.Info("Soft link: '%s' -> '%s'", path.Pretty(to), path.Pretty(from)) - - removeExisting(to) - - // create soft link - err := os.Symlink(from, to) - - // log errors - if err != nil { - log.Error(err.Error()) - } + create("Soft", from, to, os.Symlink) } // Hard - Creates a hard link between input from and to arguments func Hard(from string, to string) { - log.Info("Hard link: '%s' -> '%s'", path.Pretty(to), path.Pretty(from)) + create("Hard", from, to, os.Link) +} +func create(label, from, to string, linkFn func(string, string) error) { + log.Info("%s link: '%s' -> '%s'", label, path.Pretty(to), path.Pretty(from)) removeExisting(to) - - // create hard link - err := os.Link(from, to) - - // log errors - if err != nil { + if err := linkFn(from, to); err != nil { log.Error(err.Error()) } } diff --git a/cmd/battery-percent/root.go b/cmd/battery-percent/root.go index c5c8e90a..1cc88184 100644 --- a/cmd/battery-percent/root.go +++ b/cmd/battery-percent/root.go @@ -9,11 +9,19 @@ import ( ) func main() { - info := run.Capture("pmset -g ps") - info = strings.Split(info, "\n")[1] - // extract percent from pmset metadata - percent := strings.Fields(info)[2] - // remove trailing ; - percent = percent[:len(percent)-1] + lines := strings.Split(run.Capture("pmset -g ps"), "\n") + if len(lines) < 2 { + fmt.Println("0%") + return + } + fields := strings.Fields(lines[1]) + if len(fields) < 3 { + fmt.Println("0%") + return + } + percent := fields[2] + if len(percent) > 0 { + percent = strings.TrimRight(percent, ";") + } fmt.Println(percent) } diff --git a/cmd/battery-state/root.go b/cmd/battery-state/root.go index 7d8878cb..13a29131 100644 --- a/cmd/battery-state/root.go +++ b/cmd/battery-state/root.go @@ -10,7 +10,6 @@ import ( func main() { info := run.Capture("pmset -g ps") - info = strings.Split(info, "\n")[0] if strings.Contains(info, "AC Power") { fmt.Println("charging") } else { diff --git a/cmd/cpu/root.go b/cmd/cpu/root.go index a78f8678..5af66d10 100644 --- a/cmd/cpu/root.go +++ b/cmd/cpu/root.go @@ -12,11 +12,22 @@ import ( func main() { info := run.Capture("sysctl -n vm.loadavg") + if len(info) < 4 { + fmt.Println("0%") + return + } info = info[2 : len(info)-2] - // load averages [1 min, 5 min, 15 min] averages := strings.Split(info, " ") - average, _ := strconv.ParseFloat(averages[0], 64) + if len(averages) == 0 { + fmt.Println("0%") + return + } + average, err := strconv.ParseFloat(averages[0], 64) + if err != nil { + fmt.Println("0%") + return + } fmt.Printf("%v%%\n", math.Round(average)) } diff --git a/cmd/git-ancestor/root.go b/cmd/git-ancestor/root.go index cedef02e..388d7dbb 100644 --- a/cmd/git-ancestor/root.go +++ b/cmd/git-ancestor/root.go @@ -10,12 +10,13 @@ import ( "github.com/drn/dots/pkg/run" ) -var maxAncestors = 100 -var remote = baseRemote() -var dev = fmt.Sprintf("%s/dev", remote) -var master = fmt.Sprintf("%s/master", remote) +const maxAncestors = 100 func main() { + remote := baseRemote() + dev := fmt.Sprintf("%s/dev", remote) + master := fmt.Sprintf("%s/master", remote) + ancestors := ancestors() if len(ancestors) < 2 { @@ -28,7 +29,7 @@ func main() { continue } - identified := identify(branches) + identified := identify(branches, remote, dev, master) // if a branch is identified, use it if identified != "" { @@ -41,7 +42,7 @@ func main() { os.Exit(1) } -func identify(branches []string) string { +func identify(branches []string, remote, dev, master string) string { identified := "" for _, branch := range branches { diff --git a/cmd/gmail/root.go b/cmd/gmail/root.go index 02aba28f..c9f5786a 100644 --- a/cmd/gmail/root.go +++ b/cmd/gmail/root.go @@ -18,6 +18,7 @@ import ( "strings" "time" + "github.com/drn/dots/pkg/jsonutil" "github.com/drn/dots/pkg/path" "github.com/joho/godotenv" "github.com/spf13/cobra" @@ -151,7 +152,10 @@ func (td *tokenData) refreshIfNeeded(account string) { os.Exit(1) } var result map[string]interface{} - json.Unmarshal(body, &result) + if err := json.Unmarshal(body, &result); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to parse refresh response: %v\n", err) + return + } if at, ok := result["access_token"].(string); ok { td.Token = at } @@ -159,9 +163,15 @@ func (td *tokenData) refreshIfNeeded(account string) { td.Expiry = time.Now().Add(time.Duration(ei) * time.Second).Format(time.RFC3339) } account = resolveAccount(account) - path := filepath.Join(configDir, "tokens", account+".json") - data, _ := json.MarshalIndent(td, "", " ") - os.WriteFile(path, data, 0600) + tokenPath := filepath.Join(configDir, "tokens", account+".json") + data, err := json.MarshalIndent(td, "", " ") + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to marshal token: %v\n", err) + return + } + if err := os.WriteFile(tokenPath, data, 0600); err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to save token: %v\n", err) + } } func getAccessToken(account string) string { @@ -268,11 +278,6 @@ func openBrowser(url string) { } } -func printJSON(v interface{}) { - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - enc.Encode(v) -} func main() { var jsonFlag bool @@ -333,7 +338,7 @@ func main() { } if jsonFlag { - printJSON(results) + jsonutil.Print(results) return } if len(results) == 0 { @@ -379,7 +384,7 @@ func main() { textBody, htmlBody := extractBody(payload) if jsonFlag { - printJSON(map[string]interface{}{ + jsonutil.Print(map[string]interface{}{ "id": data["id"], "from": from, "to": to, @@ -465,7 +470,7 @@ func main() { } if jsonFlag { - printJSON(results) + jsonutil.Print(results) return } @@ -549,7 +554,7 @@ func main() { } if jsonFlag { - printJSON(accounts) + jsonutil.Print(accounts) return } for _, a := range accounts { @@ -657,7 +662,10 @@ func main() { } var result map[string]interface{} - json.Unmarshal(body, &result) + if err := json.Unmarshal(body, &result); err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse token response: %v\n", err) + os.Exit(1) + } if at, ok := result["access_token"].(string); ok { td.Token = at } @@ -676,7 +684,10 @@ func main() { } } - os.MkdirAll(filepath.Join(configDir, "tokens"), 0700) + if err := os.MkdirAll(filepath.Join(configDir, "tokens"), 0700); err != nil { + fmt.Fprintf(os.Stderr, "Failed to create tokens directory: %v\n", err) + os.Exit(1) + } data, _ := json.MarshalIndent(td, "", " ") if err := os.WriteFile(tokenPath, data, 0600); err != nil { fmt.Fprintf(os.Stderr, "Failed to save token: %v\n", err) @@ -688,5 +699,8 @@ func main() { root.PersistentFlags().BoolVar(&debugMode, "debug", false, "Show debug output for token refresh") root.AddCommand(searchCmd, readCmd, labelsCmd, accountsCmd, authCmd) - root.Execute() + if err := root.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } } diff --git a/cmd/gps/root.go b/cmd/gps/root.go index 56a1e149..cd0a1b73 100644 --- a/cmd/gps/root.go +++ b/cmd/gps/root.go @@ -3,7 +3,6 @@ package main import ( "os" - "strings" "github.com/drn/dots/pkg/cache" "github.com/drn/dots/pkg/log" @@ -12,7 +11,9 @@ import ( ) func main() { - cache.Log("gps", 15) + if cache.Log("gps", 15) { + return + } coords := coordinates() if coords == "" { os.Exit(1) @@ -27,6 +28,5 @@ func coordinates() string { } func capture(command string, args ...interface{}) string { - data := run.Capture(command+" 2>/dev/null", args...) - return strings.Trim(data, "\"") + return run.CaptureClean(command, args...) } diff --git a/cmd/ip/external.go b/cmd/ip/external.go index 64c52ad5..a82286ea 100644 --- a/cmd/ip/external.go +++ b/cmd/ip/external.go @@ -3,7 +3,6 @@ package main import ( "fmt" "os" - "strings" "github.com/drn/dots/pkg/cache" "github.com/drn/dots/pkg/log" @@ -21,8 +20,8 @@ var services = []string{ } func external(useCache bool) { - if useCache { - cache.Log("ip-external", 5) + if useCache && cache.Log("ip-external", 5) { + return } check(google()) check(opendns()) @@ -53,6 +52,5 @@ func curl(endpoint string) string { } func capture(command string, args ...interface{}) string { - data := run.Capture(command+" 2>/dev/null", args...) - return strings.Trim(data, "\"") + return run.CaptureClean(command, args...) } diff --git a/cmd/ip/home.go b/cmd/ip/home.go index 3bd4a572..75901a4c 100644 --- a/cmd/ip/home.go +++ b/cmd/ip/home.go @@ -9,8 +9,8 @@ import ( ) func home(useCache bool) { - if useCache { - cache.Log("ip-home", 5) + if useCache && cache.Log("ip-home", 5) { + return } ip := run.Capture("dig +short %s +tries=1 +time=1", os.Getenv("HOME_WAN")) if !isValid(ip) { diff --git a/cmd/slack/root.go b/cmd/slack/root.go index 4d3c5e33..ead31c78 100644 --- a/cmd/slack/root.go +++ b/cmd/slack/root.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/drn/dots/pkg/jsonutil" "github.com/drn/dots/pkg/path" "github.com/joho/godotenv" "github.com/spf13/cobra" @@ -25,23 +26,17 @@ func init() { godotenv.Load(path.FromHome(".dots/sys/env")) } -func userToken() string { - if t := os.Getenv("SLACK_XOXP_TOKEN"); t != "" { +func requiredEnv(name string) string { + if t := os.Getenv(name); t != "" { return t } - fmt.Fprintln(os.Stderr, "SLACK_XOXP_TOKEN not set") + fmt.Fprintf(os.Stderr, "%s not set\n", name) os.Exit(1) return "" } -func botToken() string { - if t := os.Getenv("SLACK_XOXB_TOKEN"); t != "" { - return t - } - fmt.Fprintln(os.Stderr, "SLACK_XOXB_TOKEN not set") - os.Exit(1) - return "" -} +func userToken() string { return requiredEnv("SLACK_XOXP_TOKEN") } +func botToken() string { return requiredEnv("SLACK_XOXB_TOKEN") } func slackGet(token, method string, params url.Values) (map[string]interface{}, error) { u := slackAPIBase + "/" + method @@ -130,11 +125,6 @@ func hoursAgoTS(hours int) string { return fmt.Sprintf("%f", float64(time.Now().Add(-time.Duration(hours)*time.Hour).Unix())) } -func printJSON(v interface{}) { - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - enc.Encode(v) -} func resolveChannelID(name string) string { if len(name) > 0 && (name[0] == 'C' || name[0] == 'G' || name[0] == 'D') { @@ -190,7 +180,7 @@ func main() { os.Exit(1) } if jsonFlag { - printJSON(data) + jsonutil.Print(data) return } fmt.Println("Authentication successful!") @@ -215,7 +205,7 @@ func main() { os.Exit(1) } if jsonFlag { - printJSON(channels) + jsonutil.Print(channels) return } fmt.Printf("Found %d channels:\n\n", len(channels)) @@ -258,7 +248,7 @@ func main() { m, _ := ch.(map[string]interface{}) if n, _ := m["name"].(string); strings.ToLower(n) == clean { if jsonFlag { - printJSON(m) + jsonutil.Print(m) return } fmt.Printf("Found: #%s (%s)\n", m["name"], m["id"]) @@ -289,7 +279,7 @@ func main() { os.Exit(1) } if jsonFlag { - printJSON(messages) + jsonutil.Print(messages) return } fmt.Printf("Last %d messages from %s:\n\n", len(messages), channelID) @@ -326,7 +316,7 @@ func main() { os.Exit(1) } if jsonFlag { - printJSON(messages) + jsonutil.Print(messages) return } fmt.Printf("Thread replies (%d):\n\n", len(messages)) @@ -368,7 +358,7 @@ func main() { msgs, _ := data["messages"].(map[string]interface{}) matches, _ := msgs["matches"].([]interface{}) if jsonFlag { - printJSON(matches) + jsonutil.Print(matches) return } fmt.Printf("Found %d messages matching '%s':\n\n", len(matches), query) @@ -414,7 +404,7 @@ func main() { } } if jsonFlag { - printJSON(active) + jsonutil.Print(active) return } fmt.Printf("Found %d users:\n\n", len(active)) @@ -460,7 +450,7 @@ func main() { strings.Contains(strings.ToLower(realName), search) || strings.Contains(strings.ToLower(displayName), search) { if jsonFlag { - printJSON(m) + jsonutil.Print(m) return } email := "" @@ -498,7 +488,7 @@ func main() { allDMs = append(allDMs, dms...) } if jsonFlag { - printJSON(allDMs) + jsonutil.Print(allDMs) return } fmt.Printf("Found %d DM conversations:\n\n", len(allDMs)) @@ -522,5 +512,8 @@ func main() { dmsCmd.Flags().BoolVar(&jsonFlag, "json", false, "JSON output") root.AddCommand(authTestCmd, channelsCmd, findChannelCmd, historyCmd, threadCmd, searchCmd, usersCmd, findUserCmd, dmsCmd) - root.Execute() + if err := root.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } } diff --git a/cmd/spotify/auth/root.go b/cmd/spotify/auth/root.go index c5aeb94f..294b5693 100644 --- a/cmd/spotify/auth/root.go +++ b/cmd/spotify/auth/root.go @@ -75,51 +75,38 @@ func Headers(accessToken string) req.Header { } } -func exchangeAuthorizationCode(code string) (string, string) { - url := "https://accounts.spotify.com/api/token" - - params := req.Param{ - "code": code, - "grant_type": "authorization_code", - "client_id": os.Getenv("SPOTIFY_CLIENT_ID"), - "client_secret": os.Getenv("SPOTIFY_CLIENT_SECRET"), - "redirect_uri": os.Getenv("SPOTIFY_REDIRECT_URI"), - } - - response, err := req.Post(url, params) - HandleRequestError(err) - - if response.Response().StatusCode != 200 { - fmt.Println(string(response.Bytes())) - os.Exit(1) - } - accessToken := jsoniter.Get(response.Bytes(), "access_token").ToString() - refreshToken := jsoniter.Get(response.Bytes(), "refresh_token").ToString() +const spotifyTokenURL = "https://accounts.spotify.com/api/token" - return accessToken, refreshToken +func exchangeAuthorizationCode(code string) (string, string) { + data := exchangeToken(req.Param{ + "code": code, + "grant_type": "authorization_code", + }) + return jsoniter.Get(data, "access_token").ToString(), + jsoniter.Get(data, "refresh_token").ToString() } func exchangeRefreshToken(code string) string { - url := "https://accounts.spotify.com/api/token" - - params := req.Param{ + data := exchangeToken(req.Param{ "refresh_token": code, "grant_type": "refresh_token", - "client_id": os.Getenv("SPOTIFY_CLIENT_ID"), - "client_secret": os.Getenv("SPOTIFY_CLIENT_SECRET"), - "redirect_uri": os.Getenv("SPOTIFY_REDIRECT_URI"), - } + }) + return jsoniter.Get(data, "access_token").ToString() +} + +func exchangeToken(params req.Param) []byte { + params["client_id"] = os.Getenv("SPOTIFY_CLIENT_ID") + params["client_secret"] = os.Getenv("SPOTIFY_CLIENT_SECRET") + params["redirect_uri"] = os.Getenv("SPOTIFY_REDIRECT_URI") - response, err := req.Post(url, params) + response, err := req.Post(spotifyTokenURL, params) HandleRequestError(err) if response.Response().StatusCode != 200 { fmt.Println(string(response.Bytes())) os.Exit(1) } - accessToken := jsoniter.Get(response.Bytes(), "access_token").ToString() - - return accessToken + return response.Bytes() } func inputCode() string { diff --git a/cmd/spotify/root.go b/cmd/spotify/root.go index 61adf03e..a2f8568b 100644 --- a/cmd/spotify/root.go +++ b/cmd/spotify/root.go @@ -20,6 +20,14 @@ import ( jsoniter "github.com/json-iterator/go" ) +const ( + spotifyTracksURL = "https://api.spotify.com/v1/me/tracks" + spotifyPlayerURL = "https://api.spotify.com/v1/me/player" + spotifyDevicesURL = "https://api.spotify.com/v1/me/player/devices" + spotifyNowPlaying = "https://api.spotify.com/v1/me/player/currently-playing" + spotifyContainsURL = "https://api.spotify.com/v1/me/tracks/contains" +) + func main() { godotenv.Load(path.FromHome(".dots/sys/env")) @@ -84,9 +92,7 @@ func alternateDeviceID(currentDeviceID string) string { } func currentDevice(accessToken string) string { - url := "https://api.spotify.com/v1/me/player/devices" - - response, err := req.Get(url, auth.Headers(accessToken)) + response, err := req.Get(spotifyDevicesURL, auth.Headers(accessToken)) auth.HandleRequestError(err) const maxDevices = 10 @@ -103,9 +109,8 @@ func currentDevice(accessToken string) string { } func transferPlayback(accessToken string, deviceID string) { - url := "https://api.spotify.com/v1/me/player" json := req.BodyJSON(map[string]interface{}{"device_ids": []string{deviceID}}) - response, err := req.Put(url, auth.Headers(accessToken), json) + response, err := req.Put(spotifyPlayerURL, auth.Headers(accessToken), json) auth.HandleRequestError(err) if response.Response().StatusCode != 204 { println(response.Dump()) @@ -116,10 +121,8 @@ func transferPlayback(accessToken string, deviceID string) { // https://developer.spotify.com/documentation/web-api/reference/library/save-tracks-user/ func saveTrack(accessToken string, trackID string) { - url := "https://api.spotify.com/v1/me/tracks" - params := req.QueryParam{"ids": trackID} - response, err := req.Put(url, auth.Headers(accessToken), params) + response, err := req.Put(spotifyTracksURL, auth.Headers(accessToken), params) auth.HandleRequestError(err) if response.Response().StatusCode != 200 { log.Error("Failed to save track") @@ -129,10 +132,8 @@ func saveTrack(accessToken string, trackID string) { // https://developer.spotify.com/documentation/web-api/reference/library/remove-tracks-user/ func removeTrack(accessToken string, trackID string) { - url := "https://api.spotify.com/v1/me/tracks" - params := req.QueryParam{"ids": trackID} - response, err := req.Delete(url, auth.Headers(accessToken), params) + response, err := req.Delete(spotifyTracksURL, auth.Headers(accessToken), params) auth.HandleRequestError(err) if response.Response().StatusCode != 200 { log.Error("Failed to remove track") @@ -142,10 +143,8 @@ func removeTrack(accessToken string, trackID string) { // https://developer.spotify.com/documentation/web-api/reference/library/check-users-saved-tracks/ func isTrackSaved(accessToken string, trackID string) bool { - url := "https://api.spotify.com/v1/me/tracks/contains" - params := req.Param{"ids": trackID} - response, err := req.Get(url, auth.Headers(accessToken), params) + response, err := req.Get(spotifyContainsURL, auth.Headers(accessToken), params) auth.HandleRequestError(err) return jsoniter.Get(response.Bytes(), 0).ToBool() @@ -153,9 +152,7 @@ func isTrackSaved(accessToken string, trackID string) bool { // https://developer.spotify.com/documentation/web-api/reference/player/get-the-users-currently-playing-track/ func currentTrackInfo(accessToken string) (string, string, string) { - url := "https://api.spotify.com/v1/me/player/currently-playing" - - response, err := req.Get(url, auth.Headers(accessToken)) + response, err := req.Get(spotifyNowPlaying, auth.Headers(accessToken)) auth.HandleRequestError(err) id := jsoniter.Get(response.Bytes(), "item", "id").ToString() name := jsoniter.Get(response.Bytes(), "item", "name").ToString() diff --git a/cmd/ssid/root.go b/cmd/ssid/root.go index 0f25b4bf..263a63e3 100644 --- a/cmd/ssid/root.go +++ b/cmd/ssid/root.go @@ -29,7 +29,7 @@ func printSSID(ssid string) { ssid = fmt.Sprintf("%s…", strings.Join(parts[:2], " ")) } if utf8.RuneCountInString(ssid) > 12 { - ssid = fmt.Sprintf("%s…", ssid[:12]) + ssid = fmt.Sprintf("%s…", string([]rune(ssid)[:12])) } } fmt.Println(ssid) diff --git a/cmd/tmux-status/color/root.go b/cmd/tmux-status/color/root.go index 056ce810..3f3a523e 100644 --- a/cmd/tmux-status/color/root.go +++ b/cmd/tmux-status/color/root.go @@ -1,12 +1,9 @@ // Package color contains tmux-status color constants package color //revive:disable-line:var-naming -// Color - -type Color string - const ( // C1 - color 1 - C1 Color = "#[fg=colour236,bg=colour103]" + C1 = "#[fg=colour236,bg=colour103]" // C1_2 - color 1 to 2 transition C1_2 = "#[fg=colour103,bg=colour239,nobold,nounderscore,noitalics]" // C1_3 - color 1 to 3 transition diff --git a/cmd/tmux-status/right/root.go b/cmd/tmux-status/right/root.go index 062b4038..58d830e4 100644 --- a/cmd/tmux-status/right/root.go +++ b/cmd/tmux-status/right/root.go @@ -101,15 +101,18 @@ func battery() string { info := strings.Split(run.Capture("pmset -g ps"), "\n") status := "battery" - if strings.Contains(info[0], "AC Power") { + if len(info) > 0 && strings.Contains(info[0], "AC Power") { status = "charging" } - // extract percent from pmset metadata - percentString := strings.Fields(info[1])[2] - // strip trailing %; - percentString = percentString[:len(percentString)-2] - percent, _ := strconv.Atoi(percentString) + percent := 0 + if len(info) > 1 { + fields := strings.Fields(info[1]) + if len(fields) > 2 { + percentString := strings.TrimRight(fields[2], "%;") + percent, _ = strconv.Atoi(percentString) + } + } color := "" switch { diff --git a/cmd/tmux-status/root.go b/cmd/tmux-status/root.go index ee06c644..799beb67 100644 --- a/cmd/tmux-status/root.go +++ b/cmd/tmux-status/root.go @@ -12,9 +12,14 @@ import ( "github.com/drn/dots/pkg/log" ) +const ( + minWidth = 90 + medWidth = 150 +) + var side string -// Position - +// Position defines the interface for responsive tmux status sections. type Position interface { Min() Med() @@ -59,9 +64,9 @@ func sides() { } switch { - case width < 90: + case width < minWidth: position.Min() - case width < 150: + case width < medWidth: position.Med() default: position.Max() diff --git a/cmd/tmux-status/separator/root.go b/cmd/tmux-status/separator/root.go index f8cf0487..8bca4e0a 100644 --- a/cmd/tmux-status/separator/root.go +++ b/cmd/tmux-status/separator/root.go @@ -1,12 +1,9 @@ // Package separator provides tmux-status separator constants package separator -// Separator - -type Separator string - const ( // R1 - right-facing full chevron - R1 Separator = "\ue0b0" + R1 = "\ue0b0" // R2 - right-facing line chevron R2 = "\ue0b1" // L1 - left-facing full chevron diff --git a/cmd/tts/root.go b/cmd/tts/root.go index 4b0a89bb..84a31733 100644 --- a/cmd/tts/root.go +++ b/cmd/tts/root.go @@ -65,7 +65,10 @@ func main() { root.Flags().Float64VarP(&speed, "speed", "s", 1.0, "Speed (0.25-4.0)") root.Flags().StringVarP(&model, "model", "m", "tts-1", "Model (tts-1, tts-1-hd)") - root.Execute() + if err := root.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } } // buildRequest constructs the OpenAI TTS API request. diff --git a/cmd/weather/openweather/root.go b/cmd/weather/openweather/root.go index 895f1e8a..867bbed4 100644 --- a/cmd/weather/openweather/root.go +++ b/cmd/weather/openweather/root.go @@ -69,7 +69,7 @@ func conditions(data string) string { func isNight(data string) bool { icon := jsoniter.Get([]byte(data), "weather", 0, "icon").ToString() - return icon[len(icon)-1:] == "n" + return len(icon) > 0 && icon[len(icon)-1:] == "n" } func fetchWeatherJSON() string { diff --git a/cmd/weather/root.go b/cmd/weather/root.go index f86c74ad..efce6d44 100644 --- a/cmd/weather/root.go +++ b/cmd/weather/root.go @@ -29,8 +29,8 @@ func main() { os.Exit(1) } - if !opts.SkipCache { - cache.Log("weather", 15) + if !opts.SkipCache && cache.Log("weather", 15) { + return } weather := weather() diff --git a/cmd/weather/wttr/root.go b/cmd/weather/wttr/root.go index fb02fa7b..a1674f47 100644 --- a/cmd/weather/wttr/root.go +++ b/cmd/weather/wttr/root.go @@ -23,6 +23,9 @@ func Info() string { } func formatTemp(temp string) string { + if len(temp) < 2 { + return temp + } return temp[1 : len(temp)-1] } diff --git a/pkg/cache/root.go b/pkg/cache/root.go index 86563699..5e65e15a 100644 --- a/pkg/cache/root.go +++ b/pkg/cache/root.go @@ -12,13 +12,14 @@ import ( ) // Log - logs data from input cache key if less than specified TTL (in minutes) -// and exits with a successful status, otherwise returns -func Log(key string, ttl float64) { +// and returns true if a cached value was found and logged +func Log(key string, ttl float64) bool { data := Read(key, ttl) if data != "" { log.Info(data) - os.Exit(0) + return true } + return false } // Warm - returns true if the last write to the cache key less than the diff --git a/pkg/jsonutil/root.go b/pkg/jsonutil/root.go new file mode 100644 index 00000000..8f9d7be3 --- /dev/null +++ b/pkg/jsonutil/root.go @@ -0,0 +1,14 @@ +// Package jsonutil provides shared JSON output helpers +package jsonutil + +import ( + "encoding/json" + "os" +) + +// Print writes v as indented JSON to stdout. +func Print(v interface{}) { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + enc.Encode(v) +} diff --git a/pkg/run/root.go b/pkg/run/root.go index 0730c0d0..e9019e3f 100644 --- a/pkg/run/root.go +++ b/pkg/run/root.go @@ -48,6 +48,12 @@ func Verbose(command string, args ...interface{}) error { return cmd.Run() } +// CaptureClean - Like Capture but suppresses stderr and trims quotes. +func CaptureClean(command string, args ...interface{}) string { + data := Capture(command+" 2>/dev/null", args...) + return strings.Trim(data, "\"") +} + // Silent - Runs the specified command without logging it first. func Silent(command string, args ...interface{}) error { resolvedCommand := fmt.Sprintf(command, args...)