Skip to content

Commit

Permalink
refactor: add cmder class
Browse files Browse the repository at this point in the history
  • Loading branch information
marianozunino committed Aug 22, 2024
1 parent 91f7e55 commit 2b58c18
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 89 deletions.
1 change: 1 addition & 0 deletions cmd/fzf.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var fzfCmd = &cobra.Command{
app.WithVerbose(confData.Verbose),
app.WithDbPath(confData.DBPath),
app.WithBlacklist(confData.BalcklistPatterns),
app.WithCommand(app.Noop),
).Fzf()
},
}
Expand Down
1 change: 1 addition & 0 deletions cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ var listCmd = &cobra.Command{
app.WithVerbose(confData.Verbose),
app.WithDbPath(confData.DBPath),
app.WithBlacklist(confData.BalcklistPatterns),
app.WithCommand(app.Noop),
).List(os.Stdout)
},
}
Expand Down
1 change: 1 addition & 0 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var syncCmd = &cobra.Command{
app.WithVerbose(confData.Verbose),
app.WithDbPath(confData.DBPath),
app.WithBlacklist(confData.BalcklistPatterns),
app.WithCommand(app.Noop),
).Sync()
},
}
Expand Down
1 change: 1 addition & 0 deletions cmd/wipe.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var wipeCmd = &cobra.Command{
app.WithAccount(confData.Email),
app.WithVerbose(confData.Verbose),
app.WithDbPath(confData.DBPath),
app.WithCommand(app.Noop),
).WipeCache()
},
}
Expand Down
9 changes: 4 additions & 5 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func WithCommand(command DMenuCommand) AppOption {
func Newapp(opts ...AppOption) *App {

p := &App{
sdmWrapper: sdm.SDMClient{Exe: "sdm"},
sdmWrapper: *sdm.NewSDMClient("sdm"),
dbPath: xdg.DataHome,
dmenuCommand: Rofi,
blacklistPatterns: []string{},
Expand Down Expand Up @@ -130,9 +130,8 @@ func ellipsize(s string, maxLen int) string {
return s[:maxLen] + "..."
}

func (p *App) retryCommand(command func() error) error {
err := command()

func (p *App) retryCommand(exec func() error) error {
err := exec()
if err == nil {
return nil
}
Expand All @@ -146,7 +145,7 @@ func (p *App) retryCommand(command func() error) error {

switch sdErr.Code {
case sdm.Unauthorized:
return p.handleUnauthorized(command)
return p.handleUnauthorized(exec)
case sdm.InvalidCredentials:
return p.handleInvalidCredentials(err)
case sdm.ResourceNotFound:
Expand Down
10 changes: 6 additions & 4 deletions internal/app/dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ func mustHaveDependencies(dmenuCommand DMenuCommand) {
}
}

log.Debug().Msg(fmt.Sprintf("Checking dmenu command: %s", dmenuCommand.String()))
_, err := exec.LookPath(dmenuCommand.String())
if err != nil {
log.Fatal().Msg(fmt.Sprintf("Dependency not found: %s", dmenuCommand.String()))
if dmenuCommand != Noop {
log.Debug().Msg(fmt.Sprintf("Checking dmenu command: %s", dmenuCommand.String()))
_, err := exec.LookPath(dmenuCommand.String())
if err != nil {
log.Fatal().Msg(fmt.Sprintf("Dependency not found: %s", dmenuCommand.String()))
}
}

log.Debug().Msg("Dependencies OK")
Expand Down
1 change: 1 addition & 0 deletions internal/app/dmenu.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type DMenuCommand string
const (
Rofi DMenuCommand = "rofi"
Wofi = "wofi"
Noop = "noop"
)

func (d DMenuCommand) String() string {
Expand Down
1 change: 1 addition & 0 deletions internal/app/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

func (p *App) Sync() error {
log.Debug().Msg("Syncing...")

statusesBuffer := new(bytes.Buffer)

if err := p.retryCommand(func() error {
Expand Down
70 changes: 70 additions & 0 deletions internal/cmder/cmder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package cmder

import (
"bytes"
"io"
"os/exec"
"strings"

"github.com/rs/zerolog/log"
)

type CommandOption func(*CommandRunner, *exec.Cmd)
type ErrorParser func(output string, err error) error

type CommandRunner struct {
Exe string
Args []string
ErrorParser ErrorParser
outputBuffer bytes.Buffer
}

func WithStdin(stdin io.Reader) CommandOption {
return func(cr *CommandRunner, cmd *exec.Cmd) {
cmd.Stdin = stdin
}
}

func WithOutput(w io.Writer) CommandOption {
return func(cr *CommandRunner, cmd *exec.Cmd) {
cmd.Stdout = io.MultiWriter(w, &cr.outputBuffer)
cmd.Stderr = io.MultiWriter(w, &cr.outputBuffer)
}
}

func WithErrorParser(parser ErrorParser) CommandOption {
return func(cr *CommandRunner, cmd *exec.Cmd) {
cr.ErrorParser = parser
}
}

func WithArgs(args ...string) CommandOption {
return func(cr *CommandRunner, cmd *exec.Cmd) {
cr.Args = args
}
}

// RunCommand executes a command with the given options
func (r *CommandRunner) RunCommand(opts ...CommandOption) error {
r.outputBuffer.Reset()

cmd := exec.Command(r.Exe)

for _, opt := range opts {
opt(r, cmd)
}

cmd.Args = append(cmd.Args, r.Args...)

log.Debug().Msgf("Running command: %s %s", r.Exe, strings.Join(r.Args, " "))

err := cmd.Run()
if err != nil {
log.Error().Msgf("Command failed: %s", err)
if r.ErrorParser != nil {
return r.ErrorParser(r.outputBuffer.String(), err)
}
return err
}
return nil
}
45 changes: 45 additions & 0 deletions internal/sdm/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package sdm

import (
"strings"

"github.com/rs/zerolog/log"
)

type SDMErrorCode int

const (
Unauthorized SDMErrorCode = iota
InvalidCredentials
Unknown
ResourceNotFound
)

type SDMError struct {
Code SDMErrorCode
Msg string
}

func (e SDMError) Error() string {
return e.Msg
}

// parseSdmError parses the output and error to return an SDMError with the appropriate code.
func parseSdmError(output string, err error) error {
log.Debug().Msgf("Parsing error: %s", output)
if err == nil {
return nil
}
switch {
case strings.Contains(output, "You are not authenticated"):
return SDMError{Code: Unauthorized, Msg: output}
case strings.Contains(output, "Cannot find datasource named"):
return SDMError{Code: ResourceNotFound, Msg: output}
case strings.Contains(output, "access denied"):
return SDMError{Code: InvalidCredentials, Msg: output}
case strings.Contains(output, "Invalid credentials"):
return SDMError{Code: InvalidCredentials, Msg: output}
default:
return SDMError{Code: Unknown, Msg: output}
}
}
111 changes: 35 additions & 76 deletions internal/sdm/sdm.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,118 +3,77 @@ package sdm
import (
"encoding/json"
"io"
"os/exec"
"strings"

"github.com/rs/zerolog/log"
"github.com/marianozunino/sdm-ui/internal/cmder"
)

type SDMClient struct {
Exe string
}

type SDMErrorCode int

const (
Unauthorized SDMErrorCode = iota
InvalidCredentials
Unknown
ResourceNotFound
)

type SDMError struct {
Code SDMErrorCode
Msg string
}

func (e SDMError) Error() string {
return e.Msg
}

type SdmReady struct {
Account *string `json:"account"`
ListenerRunning bool `json:"listener_running"`
StateLoaded bool `json:"state_loaded"`
IsLinked bool `json:"is_linked"`
}

// RunCommand executes a command, logs the SDM version, and returns the command output or an error.
func (s *SDMClient) RunCommand(args ...string) (string, error) {
log.Debug().Msgf("Running command: %s %s", s.Exe, strings.Join(args, " "))
type SDMClient struct {
CommandRunner *cmder.CommandRunner
}

// Execute the command
cmd := exec.Command(s.Exe, args...)
output, err := cmd.CombinedOutput()
if err != nil {
log.Error().Msgf("Command failed: %s (%s)", output, err)
func NewSDMClient(exe string) *SDMClient {
return &SDMClient{
CommandRunner: &cmder.CommandRunner{
Exe: exe,
ErrorParser: parseSdmError, // default error parser
},
}
return string(output), err
}

// Ready checks if the SDM client is ready and returns the state.
func (s *SDMClient) Ready() (SdmReady, error) {
output, err := s.RunCommand("ready")

var output strings.Builder
err := s.CommandRunner.RunCommand(
cmder.WithArgs("ready"),
cmder.WithOutput(&output),
)
if err != nil {
return SdmReady{}, parseSdmError(output, err)
return SdmReady{}, err
}

var ready SdmReady
if err := json.Unmarshal([]byte(output), &ready); err != nil {
if err := json.Unmarshal([]byte(output.String()), &ready); err != nil {
return SdmReady{}, err
}

return ready, nil
}

// Logout logs out the user from the SDM client.
func (s *SDMClient) Logout() error {
output, err := s.RunCommand("logout")
return parseSdmError(output, err)
return s.CommandRunner.RunCommand(
cmder.WithArgs("logout"),
)
}

// Login logs in the user with the provided email and password.
func (s *SDMClient) Login(email, password string) error {
cmd := exec.Command(s.Exe, "login", "--email", email)
cmd.Stdin = strings.NewReader(password + "\n")
output, err := cmd.CombinedOutput()
if err != nil {
log.Debug().Msg(string(err.Error()))
}
return parseSdmError(string(output), err)
stdin := strings.NewReader(password + "\n")
return s.CommandRunner.RunCommand(
cmder.WithArgs("login", "--email", email),
cmder.WithStdin(stdin),
cmder.WithErrorParser(parseSdmError),
)
}

// Status writes the status of the SDM client to the provided writer.
func (s *SDMClient) Status(w io.Writer) error {
output, err := s.RunCommand("status", "-j")
if _, writeErr := w.Write([]byte(output)); writeErr != nil {
return writeErr
}
return parseSdmError(output, err)
func (s *SDMClient) Status(output io.Writer) error {
return s.CommandRunner.RunCommand(
cmder.WithArgs("status", "-j"),
cmder.WithOutput(output),
cmder.WithErrorParser(parseSdmError),
)
}

// Connect connects to the specified data source.
func (s *SDMClient) Connect(dataSource string) error {
output, err := s.RunCommand("connect", dataSource)
return parseSdmError(output, err)
}

// parseSdmError parses the output and error to return an SDMError with the appropriate code.
func parseSdmError(output string, err error) error {
if err == nil {
return nil
}

switch {
case strings.Contains(output, "You are not authenticated"):
return SDMError{Code: Unauthorized, Msg: output}
case strings.Contains(output, "Cannot find datasource named"):
return SDMError{Code: ResourceNotFound, Msg: output}
case strings.Contains(output, "access denied"):
return SDMError{Code: InvalidCredentials, Msg: output}
case strings.Contains(output, "Invalid credentials"):
return SDMError{Code: InvalidCredentials, Msg: output}
default:
return SDMError{Code: Unknown, Msg: output}
}
return s.CommandRunner.RunCommand(
cmder.WithArgs("connect", dataSource),
)
}
Loading

0 comments on commit 2b58c18

Please sign in to comment.