mirror of
https://github.com/anotherhadi/default-creds-tui.git
synced 2026-10-05 19:08:25 +02:00
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/anotherhadi/ilovetui/app"
|
||||
"github.com/anotherhadi/ilovetui/minsize"
|
||||
"github.com/anotherhadi/ilovetui/modal"
|
||||
"github.com/anotherhadi/ilovetui/notification"
|
||||
zone "github.com/lrstanley/bubblezone/v2"
|
||||
|
||||
"github.com/anotherhadi/default-creds-tui/internal/creds"
|
||||
"github.com/anotherhadi/default-creds-tui/ui"
|
||||
)
|
||||
|
||||
const (
|
||||
minWidth = 25
|
||||
minHeight = 15
|
||||
)
|
||||
|
||||
type appModel struct {
|
||||
core ui.Model
|
||||
modals modal.Model
|
||||
notif notification.Model
|
||||
minsize minsize.Model
|
||||
width, height int
|
||||
}
|
||||
|
||||
func newApp(mode creds.Mode, apiURL string, printOnEnter bool) appModel {
|
||||
zone.NewGlobal()
|
||||
return appModel{
|
||||
core: ui.New(mode, apiURL, printOnEnter),
|
||||
modals: modal.New(),
|
||||
notif: notification.New(),
|
||||
minsize: minsize.New(minWidth, minHeight),
|
||||
}
|
||||
}
|
||||
|
||||
func (a appModel) Init() tea.Cmd {
|
||||
return tea.Batch(a.core.Init(), a.modals.Init(), a.notif.Init())
|
||||
}
|
||||
|
||||
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if _, ok := msg.(app.QuitMsg); ok {
|
||||
return a, tea.Quit
|
||||
}
|
||||
if sizeMsg, ok := msg.(tea.WindowSizeMsg); ok {
|
||||
a.width, a.height = sizeMsg.Width, sizeMsg.Height
|
||||
}
|
||||
|
||||
a.core.ModalOpen = a.modals.Open()
|
||||
var coreCmd tea.Cmd
|
||||
a.core, coreCmd = a.core.Update(msg)
|
||||
|
||||
var modalCmd, notifCmd tea.Cmd
|
||||
a.modals, modalCmd = a.modals.Update(msg)
|
||||
a.notif, notifCmd = a.notif.Update(msg)
|
||||
|
||||
return a, tea.Batch(coreCmd, modalCmd, notifCmd)
|
||||
}
|
||||
|
||||
func (a appModel) View() tea.View {
|
||||
if !a.minsize.Fits(a.width, a.height) {
|
||||
return tea.View{
|
||||
Content: a.minsize.View(a.width, a.height),
|
||||
AltScreen: true,
|
||||
WindowTitle: "Default Creds TUI",
|
||||
}
|
||||
}
|
||||
|
||||
bg := lipgloss.Place(a.width, a.height, lipgloss.Left, lipgloss.Top, padLines(a.core.View(), a.width))
|
||||
bg = a.modals.Render(bg)
|
||||
bg = a.notif.Render(bg)
|
||||
bg = zone.Scan(bg)
|
||||
return tea.View{
|
||||
Content: bg,
|
||||
AltScreen: true,
|
||||
WindowTitle: "Default Creds TUI",
|
||||
MouseMode: tea.MouseModeCellMotion,
|
||||
}
|
||||
}
|
||||
|
||||
func padLines(s string, width int) string {
|
||||
lines := strings.Split(s, "\n")
|
||||
for i, l := range lines {
|
||||
if w := lipgloss.Width(l); w < width {
|
||||
lines[i] = l + strings.Repeat(" ", width-w)
|
||||
}
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
|
||||
"github.com/anotherhadi/default-creds-tui/internal/creds"
|
||||
)
|
||||
|
||||
func runSearchCLI(mode creds.Mode, apiURL, query string, noColors bool) error {
|
||||
var entries []creds.Entry
|
||||
switch mode {
|
||||
case creds.Local:
|
||||
all, err := creds.LoadLocal(creds.LocalDataDir())
|
||||
if err != nil {
|
||||
return fmt.Errorf("local data: %w", err)
|
||||
}
|
||||
entries = creds.Filter(all, query)
|
||||
default:
|
||||
found, _, err := creds.FetchOnline(apiURL, query, 50)
|
||||
if err != nil {
|
||||
return fmt.Errorf("online search: %w", err)
|
||||
}
|
||||
entries = found
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
fmt.Printf("No results for %q.\n", query)
|
||||
return nil
|
||||
}
|
||||
|
||||
blocks := make([]string, len(entries))
|
||||
for i, e := range entries {
|
||||
blocks[i] = renderEntryCLI(e, noColors)
|
||||
}
|
||||
fmt.Println(strings.Join(blocks, "\n\n"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderEntryCLI(e creds.Entry, noColors bool) string {
|
||||
manufacturerStyle := lipgloss.NewStyle().Bold(true)
|
||||
nameStyle := lipgloss.NewStyle().Bold(true)
|
||||
versionStyle := lipgloss.NewStyle()
|
||||
credStyle := lipgloss.NewStyle()
|
||||
commentStyle := lipgloss.NewStyle().Italic(true)
|
||||
tagStyle := lipgloss.NewStyle()
|
||||
|
||||
if !noColors {
|
||||
manufacturerStyle = manufacturerStyle.Foreground(style.S.Subtle)
|
||||
nameStyle = nameStyle.Foreground(style.S.Primary)
|
||||
versionStyle = versionStyle.Foreground(style.S.Subtle)
|
||||
credStyle = credStyle.Foreground(style.S.Success)
|
||||
commentStyle = commentStyle.Foreground(style.S.Muted)
|
||||
tagStyle = tagStyle.Foreground(style.S.Subtle)
|
||||
}
|
||||
|
||||
titleParts := []string{manufacturerStyle.Render(strings.ToUpper(e.Manufacturer))}
|
||||
if e.Version != "" && e.Version != "all" {
|
||||
titleParts = append(titleParts, versionStyle.Render("v"+e.Version))
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
strings.Join(titleParts, " · "),
|
||||
nameStyle.Render(e.Name),
|
||||
}
|
||||
|
||||
var credLines []string
|
||||
if e.User != "" {
|
||||
credLines = append(credLines, credStyle.Render("user: "+e.User))
|
||||
}
|
||||
if e.Pass != "" {
|
||||
credLines = append(credLines, credStyle.Render("pass: "+e.Pass))
|
||||
}
|
||||
if len(credLines) > 0 {
|
||||
lines = append(lines, strings.Join(credLines, " "))
|
||||
}
|
||||
|
||||
if e.Comment != "" {
|
||||
lines = append(lines, commentStyle.Render(e.Comment))
|
||||
} else if len(e.Tags) > 0 {
|
||||
lines = append(lines, tagStyle.Render("#"+strings.Join(e.Tags, " #")))
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
zone "github.com/lrstanley/bubblezone/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/anotherhadi/default-creds-tui/internal/config"
|
||||
"github.com/anotherhadi/default-creds-tui/internal/creds"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
flagConfig string
|
||||
flagAddDefaultConfig bool
|
||||
flagMode string
|
||||
flagSearch string
|
||||
flagNoColors bool
|
||||
flagPrint bool
|
||||
)
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "default-creds-tui",
|
||||
Short: "Search factory-set default credentials, without leaving the terminal.",
|
||||
Version: version,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
home, _ := os.UserHomeDir()
|
||||
cfgPath := filepath.Join(home, ".config", "default-creds-tui", "config.yaml")
|
||||
if flagConfig != "" {
|
||||
cfgPath = flagConfig
|
||||
}
|
||||
|
||||
if flagAddDefaultConfig {
|
||||
if err := config.WriteDefaultConfig(cfgPath); err != nil {
|
||||
return fmt.Errorf("add-default-config: %w", err)
|
||||
}
|
||||
fmt.Printf("default config written to %s\n", cfgPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := config.Load(cfgPath); err != nil {
|
||||
return fmt.Errorf("config: %w", err)
|
||||
}
|
||||
config.Global.Version = version
|
||||
|
||||
modeStr := config.Global.App.Mode
|
||||
if flagMode != "" {
|
||||
modeStr = flagMode
|
||||
}
|
||||
mode, err := creds.ParseMode(modeStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mode: %w", err)
|
||||
}
|
||||
mode = creds.Resolve(mode)
|
||||
|
||||
style.Init()
|
||||
|
||||
if flagSearch != "" {
|
||||
return runSearchCLI(mode, config.Global.App.ApiURL, flagSearch, flagNoColors)
|
||||
}
|
||||
|
||||
zone.NewGlobal()
|
||||
|
||||
printOnEnter := config.Global.App.PrintOnEnter || flagPrint
|
||||
|
||||
p := tea.NewProgram(newApp(mode, config.Global.App.ApiURL, printOnEnter))
|
||||
finalModel, err := p.Run()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if am, ok := finalModel.(appModel); ok {
|
||||
if e, ok := am.core.PrintEntry(); ok {
|
||||
fmt.Println(renderEntryCLI(e, flagNoColors))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
rootCmd.Flags().StringVarP(&flagConfig, "config", "c", "", "path to config file")
|
||||
rootCmd.Flags().BoolVar(&flagAddDefaultConfig, "add-default-config", false, "copy the default config file to the config path and exit")
|
||||
rootCmd.Flags().StringVarP(&flagMode, "mode", "m", "", "credential source: auto, online or local")
|
||||
rootCmd.Flags().StringVarP(&flagSearch, "search", "s", "", "search from the CLI and print results directly, without launching the TUI")
|
||||
rootCmd.Flags().BoolVar(&flagNoColors, "no-colors", false, "disable colored output (used with --search)")
|
||||
rootCmd.Flags().BoolVar(&flagPrint, "print", false, "pressing enter prints the selected result to stdout and exits, instead of copying the password")
|
||||
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
if version != "dev" {
|
||||
return
|
||||
}
|
||||
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" && info.Main.Version != "(devel)" {
|
||||
version = info.Main.Version
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user