diff --git a/auth/auth_ui/huh.go b/auth/auth_ui/huh.go index 1b9bc4dd..80d48e51 100644 --- a/auth/auth_ui/huh.go +++ b/auth/auth_ui/huh.go @@ -1,12 +1,14 @@ package auth_ui import ( + "context" "errors" "fmt" "io" "regexp" "strconv" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/huh" "github.com/rusq/slackauth" ) @@ -33,7 +35,7 @@ func (h *Huh) RequestWorkspace(w io.Writer) (string, error) { func (*Huh) Stop() {} -func (*Huh) RequestCreds(w io.Writer, workspace string) (email string, passwd string, err error) { +func (*Huh) RequestCreds(ctx context.Context, w io.Writer, workspace string) (email string, passwd string, err error) { f := huh.NewForm( huh.NewGroup( huh.NewInput(). @@ -46,8 +48,8 @@ func (*Huh) RequestCreds(w io.Writer, workspace string) (email string, passwd st Placeholder("your slack password"). Validate(valRequired).EchoMode(huh.EchoModePassword), ), - ).WithTheme(Theme) - err = f.Run() + ).WithTheme(Theme).WithKeyMap(keymap) + err = f.RunWithContext(ctx) return } @@ -85,15 +87,13 @@ type LoginOpts struct { BrowserPath string } -func valWorkspace(s string) error { - if err := valRequired(s); err != nil { - return err - } - _, err := Sanitize(s) - return err +var keymap = huh.NewDefaultKeyMap() + +func init() { + keymap.Quit = key.NewBinding(key.WithKeys("esc", "ctrl+c"), key.WithHelp("esc", "Quit")) } -func (*Huh) RequestLoginType(w io.Writer, workspace string) (LoginOpts, error) { +func (*Huh) RequestLoginType(ctx context.Context, w io.Writer, workspace string) (LoginOpts, error) { var ret = LoginOpts{ Workspace: workspace, Type: LInteractive, @@ -120,7 +120,11 @@ func (*Huh) RequestLoginType(w io.Writer, workspace string) (LoginOpts, error) { fields = append(fields, huh.NewSelect[LoginType](). TitleFunc(func() string { - return fmt.Sprintf("Select login type for [%s]", ret.Workspace) + wsp, err := Sanitize(ret.Workspace) + if err != nil { + return "Select login type" + } + return fmt.Sprintf("Select login type for [%s]", wsp) }, &ret.Workspace). Options(opts...). Value(&ret.Type). @@ -139,21 +143,31 @@ func (*Huh) RequestLoginType(w io.Writer, workspace string) (LoginOpts, error) { return "" } }, &ret.Type)) - if err := huh.NewForm(huh.NewGroup(fields...)).WithTheme(Theme).Run(); err != nil { + + form := huh.NewForm(huh.NewGroup(fields...)).WithTheme(Theme).WithKeyMap(keymap) + + if err := form.RunWithContext(ctx); err != nil { return ret, err } + var err error + ret.Workspace, err = Sanitize(ret.Workspace) + if err != nil { + return ret, err + } + if ret.Type == LUserBrowser { - path, err := chooseBrowser() + path, err := chooseBrowser(ctx) if err != nil { return ret, err } ret.BrowserPath = path return ret, err } + return ret, nil } -func chooseBrowser() (string, error) { +func chooseBrowser(ctx context.Context) (string, error) { browsers, err := slackauth.ListBrowsers() if err != nil { return "", err @@ -172,7 +186,7 @@ func chooseBrowser() (string, error) { DescriptionFunc(func() string { return browsers[selection].Path }, &selection), - )).WithTheme(Theme).Run() + )).WithTheme(Theme).WithKeyMap(keymap).RunWithContext(ctx) if err != nil { return "", err } diff --git a/auth/auth_ui/validation.go b/auth/auth_ui/validation.go index 94e019bc..1b2b9e44 100644 --- a/auth/auth_ui/validation.go +++ b/auth/auth_ui/validation.go @@ -100,3 +100,11 @@ func valSepEaster() func(v LoginType) error { return nil } } + +func valWorkspace(s string) error { + if err := valRequired(s); err != nil { + return err + } + _, err := Sanitize(s) + return err +} diff --git a/auth/rod.go b/auth/rod.go index 41ad8558..f415ab46 100644 --- a/auth/rod.go +++ b/auth/rod.go @@ -64,10 +64,10 @@ type browserAuthUIExt interface { // RequestLoginType should request the login type from the user and return // one of the [auth_ui.LoginType] constants. The implementation should // provide a way to cancel the login flow, returning [auth_ui.LoginCancel]. - RequestLoginType(w io.Writer, workspace string) (auth_ui.LoginOpts, error) + RequestLoginType(ctx context.Context, w io.Writer, workspace string) (auth_ui.LoginOpts, error) // RequestCreds should request the user's email and password and return // them. - RequestCreds(w io.Writer, workspace string) (email string, passwd string, err error) + RequestCreds(ctx context.Context, w io.Writer, workspace string) (email string, passwd string, err error) // ConfirmationCode should request the confirmation code from the user and // return it. ConfirmationCode(email string) (code int, err error) @@ -95,7 +95,7 @@ func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) { r.opts.workspace = wsp } - resp, err := r.opts.ui.RequestLoginType(os.Stdout, r.opts.workspace) + resp, err := r.opts.ui.RequestLoginType(ctx, os.Stdout, r.opts.workspace) if err != nil { return r, err } @@ -142,7 +142,7 @@ func NewRODAuth(ctx context.Context, opts ...Option) (RodAuth, error) { } func headlessFlow(ctx context.Context, cl *slackauth.Client, workspace string, ui browserAuthUIExt) (sp simpleProvider, err error) { - username, password, err := ui.RequestCreds(os.Stdout, workspace) + username, password, err := ui.RequestCreds(ctx, os.Stdout, workspace) if err != nil { return sp, err } diff --git a/cmd/slackdump/internal/ui/bubbles/menu/model.go b/cmd/slackdump/internal/ui/bubbles/menu/model.go index 032d8092..a810b25b 100644 --- a/cmd/slackdump/internal/ui/bubbles/menu/model.go +++ b/cmd/slackdump/internal/ui/bubbles/menu/model.go @@ -160,6 +160,18 @@ func (m *Model) View() string { return m.view() } +func (m *Model) Select(id string) { + if id == m.items[m.cursor].ID { + return + } + for i, item := range m.items { + if item.ID == id && !item.Separator { + m.cursor = i + break + } + } +} + func capfirst(s string) string { if s == "" { return "" diff --git a/cmd/slackdump/internal/workspace/workspaceui/ezlogin3000.go b/cmd/slackdump/internal/workspace/workspaceui/ezlogin3000.go index cee58ba0..e5ec6687 100644 --- a/cmd/slackdump/internal/workspace/workspaceui/ezlogin3000.go +++ b/cmd/slackdump/internal/workspace/workspaceui/ezlogin3000.go @@ -18,8 +18,8 @@ func ezLogin3000(ctx context.Context, mgr manager) error { ) form := huh.NewForm(huh.NewGroup( huh.NewConfirm(). - Title("Use legacy EZ-Login?"). - Description("Do you want to use the legacy login?"). + Title("Do you want to use the legacy login?"). + Description("Choose 'Yes' if you had problems in the past with the current EZ-Login."). Value(&legacy), )).WithTheme(ui.HuhTheme()).WithKeyMap(ui.DefaultHuhKeymap) if err := form.RunWithContext(ctx); err != nil { @@ -28,11 +28,20 @@ func ezLogin3000(ctx context.Context, mgr manager) error { } return err } + + var err error if legacy { - return playwrightLogin(ctx, mgr) + err = playwrightLogin(ctx, mgr) + } else { + err = rodLogin(ctx, mgr) } - return rodLogin(ctx, mgr) - + if err != nil { + if errors.Is(err, auth.ErrCancelled) { + return nil + } + return err + } + return nil } func playwrightLogin(ctx context.Context, mgr manager) error { diff --git a/cmd/slackdump/internal/workspace/workspaceui/filesecrets.go b/cmd/slackdump/internal/workspace/workspaceui/filesecrets.go index 401e97f2..d2b59c30 100644 --- a/cmd/slackdump/internal/workspace/workspaceui/filesecrets.go +++ b/cmd/slackdump/internal/workspace/workspaceui/filesecrets.go @@ -26,7 +26,7 @@ func fileWithSecrets(ctx context.Context, mgr manager) error { ShowPermissions(true). Value(&filename). Validate(validateSecrets), - )).WithTheme(ui.HuhTheme()).WithHeight(10) + )).WithTheme(ui.HuhTheme()).WithHeight(10).WithKeyMap(ui.DefaultHuhKeymap) if err := form.RunWithContext(ctx); err != nil { if errors.Is(err, huh.ErrUserAborted) { return nil diff --git a/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go b/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go index 40fd068f..6c85766f 100644 --- a/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go +++ b/cmd/slackdump/internal/workspace/workspaceui/workspaceui.go @@ -31,12 +31,12 @@ func WorkspaceNew(ctx context.Context, _ *base.Command, _ []string) error { { ID: actLogin, Name: "Login in Browser", - Help: "Login to Slack in your browser", + Help: "Opens the browser and lets you login in a familiar way.", }, { ID: actToken, Name: "Token/Cookie", - Help: "Enter token and cookie that you grabbed from the browser", + Help: "Enter token and cookie that you grabbed from the browser.", }, { ID: actTokenFile, @@ -66,12 +66,15 @@ func WorkspaceNew(ctx context.Context, _ *base.Command, _ []string) error { actSecrets: fileWithSecrets, } + var lastID string = actLogin LOOP: for { m := menu.New("New Workspace", items, true) + m.Select(lastID) if _, err := tea.NewProgram(&wizModel{m: m}, tea.WithContext(ctx)).Run(); err != nil { return err } + lastID = m.Selected.ID if m.Cancelled { break LOOP }