├── Makefile ├── README.md ├── crypto.go ├── crypto_test.go ├── ezpwd └── main.go ├── ezpwd_tui ├── colors.go ├── colors_easyjson.go ├── main.go ├── package.go ├── password_mgmt_form.go ├── passwords_form.go └── passwords_table.go ├── go.mod ├── render.go ├── render_test.go ├── storage.go └── storage_test.go /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY=dist clean darwin linux windows 2 | BUILDARGS=-ldflags='-w -s' -trimpath 3 | 4 | all: dist darwin linux windows 5 | 6 | clean: 7 | $(RM) -fvr dist 8 | 9 | dist: 10 | mkdir dist 11 | 12 | darwin: 13 | CGO_ENABLED=0 GOOS=darwin go build ${BUILDARGS} -o dist/ezpwd_macos ./ezpwd/ 14 | CGO_ENABLED=0 GOOS=darwin go build ${BUILDARGS} -o dist/ezpwd_tui_macos ./ezpwd_tui/ 15 | 16 | linux: 17 | CGO_ENABLED=0 GOOS=linux go build ${BUILDARGS} -o dist/ezpwd_linux ./ezpwd/ 18 | CGO_ENABLED=0 GOOS=linux go build ${BUILDARGS} -o dist/ezpwd_tui_linux ./ezpwd_tui/ 19 | 20 | windows: 21 | CGO_ENABLED=0 GOOS=windows go build ${BUILDARGS} -o dist/ezpwd_windows ./ezpwd/ 22 | CGO_ENABLED=0 GOOS=windows go build ${BUILDARGS} -o dist/ezpwd_tui_windows ./ezpwd_tui/ 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EZPwd Encrypted password manager compatible with GnuPGP/OpenPGP 2 | 3 | A small utility that helps to keep passwords organized in an encrypted file. 4 | 5 | The password file format: 6 | 7 | ``` 8 | service / username / password / comment 9 | ``` 10 | 11 | Easy to grep. 12 | Also could be read by `gpg -d ${filename}` 13 | 14 | More info on [https://ezpwd.jdevelop.com/](https://ezpwd.jdevelop.com/) 15 | 16 | ### Build & install 17 | 18 | `go install ./...` 19 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package ezpwd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "golang.org/x/crypto/openpgp" 8 | ) 9 | 10 | type Crypto struct { 11 | keyPass []byte 12 | } 13 | 14 | func NewCrypto(pwd []byte) (*Crypto, error) { 15 | return &Crypto{ 16 | keyPass: pwd, 17 | }, nil 18 | } 19 | 20 | type KeyWriter io.Writer 21 | 22 | type CryptoInterface interface { 23 | Encrypt(in io.Reader, out io.Writer) error 24 | Decrypt(in io.Reader, out io.Writer) error 25 | } 26 | 27 | func (cr *Crypto) Encrypt(in io.Reader, out io.Writer) error { 28 | w, err := openpgp.SymmetricallyEncrypt(out, cr.keyPass, nil, nil) 29 | if err != nil { 30 | return fmt.Errorf("can't encrypt the message: %w", err) 31 | } 32 | if _, err = io.Copy(w, in); err != nil { 33 | return fmt.Errorf("can't copy encrypted message: %w", err) 34 | } 35 | return w.Close() 36 | } 37 | 38 | var ( 39 | noSymmetric = fmt.Errorf("Symmetric not set") 40 | wrongPass = fmt.Errorf("Wrong password") 41 | ) 42 | 43 | func (cr *Crypto) Decrypt(in io.Reader, out io.Writer) error { 44 | read := false 45 | md, err := openpgp.ReadMessage(in, nil, func(keys []openpgp.Key, symmetric bool) ([]byte, error) { 46 | if !symmetric { 47 | return nil, noSymmetric 48 | } 49 | if read { 50 | return nil, wrongPass 51 | } 52 | read = true 53 | return cr.keyPass, nil 54 | }, nil) 55 | if err != nil { 56 | return fmt.Errorf("can't decrypt message : %w", err) 57 | } 58 | if _, err := io.Copy(out, md.UnverifiedBody); err != nil { 59 | return fmt.Errorf("can't transfer decrypted message : %w", err) 60 | } 61 | return nil 62 | } 63 | 64 | var _ CryptoInterface = &Crypto{} 65 | -------------------------------------------------------------------------------- /crypto_test.go: -------------------------------------------------------------------------------- 1 | package ezpwd 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestEncrypt(t *testing.T) { 12 | crypto, err := NewCrypto([]byte("password")) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | var b, dec bytes.Buffer 18 | 19 | err = crypto.Encrypt(strings.NewReader("test message"), &b) 20 | require.Nil(t, err) 21 | 22 | err = crypto.Decrypt(&b, &dec) 23 | require.Nil(t, err) 24 | require.EqualValues(t, "test message", string(dec.Bytes())) 25 | 26 | } 27 | 28 | func TestDecryptWrongPass(t *testing.T) { 29 | crypto, err := NewCrypto([]byte("password")) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | b := new(bytes.Buffer) 35 | dec := new(bytes.Buffer) 36 | 37 | err = crypto.Encrypt(strings.NewReader("test message"), b) 38 | require.Nil(t, err) 39 | 40 | crypto.keyPass = []byte("passwor") 41 | err = crypto.Decrypt(b, dec) 42 | require.NotNil(t, err) 43 | } 44 | -------------------------------------------------------------------------------- /ezpwd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "strconv" 13 | "syscall" 14 | "time" 15 | 16 | "os/user" 17 | 18 | "github.com/atotto/clipboard" 19 | "github.com/jdevelop/ezpwd" 20 | "golang.org/x/crypto/ssh/terminal" 21 | ) 22 | 23 | var ( 24 | add = flag.Bool("add", false, "Add new password") 25 | list = flag.Bool("list", false, "List all passwords") 26 | passFile = flag.String("passfile", "private/pass.enc", "Password file") 27 | upd = flag.Bool("update", false, "Update password") 28 | ) 29 | 30 | func main() { 31 | 32 | flag.Parse() 33 | 34 | u, err := user.Current() 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | encPath := filepath.Join(u.HomeDir, *passFile) 40 | dir := filepath.Dir(encPath) 41 | 42 | switch d, err := os.Stat(dir); err { 43 | case nil: 44 | if !d.IsDir() { 45 | log.Fatalf("Can't use folder '%s'", dir) 46 | } 47 | case os.ErrNotExist: 48 | if err := os.MkdirAll(dir, 0700); err != nil { 49 | log.Fatalf("Can't create folder '%s'", dir) 50 | } 51 | default: 52 | log.Fatalf("Fatal error, aborting: %+v", err) 53 | } 54 | 55 | fmt.Print("Storage Password :/> ") 56 | bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) 57 | 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | fmt.Println() 62 | 63 | crypto, err := ezpwd.NewCrypto(bytePassword) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | 68 | switch { 69 | case *add: 70 | if err := addFunc(crypto, encPath); err != nil { 71 | log.Fatal(err) 72 | } 73 | case *list: 74 | rdr := ezpwd.NewShownPWDs(os.Stdout) 75 | if err := listFunc(crypto, rdr, encPath); err != nil { 76 | log.Fatal(err) 77 | } 78 | case *upd: 79 | rdr := ezpwd.NewHiddenPWDs(os.Stdout) 80 | if err := updateFunc(crypto, rdr, encPath); err != nil { 81 | log.Fatal(err) 82 | } 83 | default: 84 | rdr := ezpwd.NewHiddenPWDs(os.Stdout) 85 | if err := listAndCopyFunc(crypto, rdr, encPath); err != nil { 86 | log.Fatal(err) 87 | } 88 | } 89 | 90 | } 91 | 92 | func listPasswords(cr ezpwd.CryptoInterface, file string) ([]ezpwd.Password, error) { 93 | var ( 94 | f *os.File 95 | err error 96 | ) 97 | if f, err = os.Open(file); err != nil { 98 | return nil, err 99 | } 100 | 101 | buffer := new(bytes.Buffer) 102 | 103 | if err := cr.Decrypt(f, buffer); err != nil { 104 | return nil, err 105 | } 106 | 107 | return ezpwd.ReadPasswords(buffer) 108 | } 109 | 110 | func listAndCopyFunc(cr ezpwd.CryptoInterface, rdr ezpwd.Renderer, file string) error { 111 | pwds, err := listPasswords(cr, file) 112 | if err != nil { 113 | return err 114 | } 115 | rdr.RenderPasswords(pwds) 116 | fmt.Printf("Choose password ") 117 | s := bufio.NewScanner(os.Stdin) 118 | s.Scan() 119 | selection, err := strconv.ParseInt(s.Text(), 10, 64) 120 | if err != nil { 121 | return err 122 | } 123 | if selection >= 0 && int(selection) < len(pwds) { 124 | return clipboard.WriteAll(pwds[selection].Password) 125 | } 126 | return nil 127 | } 128 | 129 | func listFunc(cr ezpwd.CryptoInterface, rdr ezpwd.Renderer, file string) error { 130 | pwds, err := listPasswords(cr, file) 131 | if err != nil { 132 | return err 133 | } 134 | rdr.RenderPasswords(pwds) 135 | return nil 136 | } 137 | 138 | func readInput(pwd *ezpwd.Password) error { 139 | scanner := bufio.NewScanner(os.Stdin) 140 | fmt.Print("Service :/> ") 141 | if scanner.Scan() { 142 | if s := scanner.Text(); s != "" { 143 | pwd.Service = s 144 | } 145 | } 146 | 147 | fmt.Print("Username/email :/> ") 148 | if scanner.Scan() { 149 | if s := scanner.Text(); s != "" { 150 | pwd.Login = s 151 | } 152 | } 153 | for { 154 | fmt.Print("Enter Password :/> ") 155 | bytePassword, err := terminal.ReadPassword(int(syscall.Stdin)) 156 | if err != nil { 157 | return err 158 | } 159 | fmt.Print("\nConfirm Password :/> ") 160 | confirmPassword, err := terminal.ReadPassword(int(syscall.Stdin)) 161 | if err != nil { 162 | return err 163 | } 164 | if string(bytePassword) == string(confirmPassword) { 165 | if s := string(bytePassword); s != "" { 166 | pwd.Password = s 167 | } 168 | break 169 | } else { 170 | fmt.Println() 171 | } 172 | } 173 | 174 | fmt.Print("\nComment :/> ") 175 | if scanner.Scan() { 176 | if s := scanner.Text(); s != "" { 177 | pwd.Comment = s 178 | } 179 | } 180 | return nil 181 | } 182 | 183 | func readPasswords(cr ezpwd.CryptoInterface, file string) ([]ezpwd.Password, *os.File, error) { 184 | var ( 185 | _file *os.File 186 | pwds []ezpwd.Password 187 | ) 188 | if _, err := os.Stat(file); err != nil { 189 | if os.IsNotExist(err) { 190 | if _file, err = os.Create(file); err != nil { 191 | return nil, nil, err 192 | } 193 | } else { 194 | return nil, nil, err 195 | } 196 | pwds = make([]ezpwd.Password, 0, 1) 197 | } else { 198 | if _file, err = os.Open(file); err != nil { 199 | return nil, nil, err 200 | } 201 | if pwds, err = listPasswords(cr, file); err != nil { 202 | return nil, nil, err 203 | } 204 | } 205 | return pwds, _file, nil 206 | } 207 | 208 | func updateFunc(cr ezpwd.CryptoInterface, render ezpwd.Renderer, file string) error { 209 | pwds, _file, err := readPasswords(cr, file) 210 | if err != nil { 211 | return err 212 | } 213 | render.RenderPasswords(pwds) 214 | scanner := bufio.NewScanner(os.Stdin) 215 | var ( 216 | idx int 217 | ) 218 | for { 219 | fmt.Print("Please choose the entry you'd like to change: ") 220 | scanner.Scan() 221 | numStr := scanner.Text() 222 | _idx, err := strconv.ParseInt(numStr, 10, 32) 223 | if err != nil { 224 | return err 225 | } 226 | idx = int(_idx) 227 | if l := len(pwds); l <= idx { 228 | fmt.Printf("Enter numbers between 0 and %d\n", l) 229 | } else { 230 | break 231 | } 232 | } 233 | err = readInput(&pwds[idx]) 234 | if err != nil { 235 | return err 236 | } 237 | if err := backup(file); err != nil { 238 | return err 239 | } 240 | buffer := new(bytes.Buffer) 241 | if err := ezpwd.WritePasswords(pwds, buffer); err != nil { 242 | return err 243 | } 244 | if err := _file.Close(); err != nil { 245 | return err 246 | } 247 | _file, err = os.OpenFile(file, os.O_WRONLY, os.ModeAppend) 248 | if err != nil { 249 | return err 250 | } 251 | return cr.Encrypt(buffer, _file) 252 | } 253 | 254 | func addFunc(cr ezpwd.CryptoInterface, file string) error { 255 | 256 | pwds, _file, err := readPasswords(cr, file) 257 | if err != nil { 258 | return err 259 | } 260 | 261 | var pwd ezpwd.Password 262 | 263 | err = readInput(&pwd) 264 | 265 | if err != nil { 266 | return err 267 | } 268 | 269 | pwds = append(pwds, pwd) 270 | 271 | if err := backup(file); err != nil { 272 | return err 273 | } 274 | 275 | buffer := new(bytes.Buffer) 276 | 277 | if err := ezpwd.WritePasswords(pwds, buffer); err != nil { 278 | return err 279 | } 280 | 281 | if err := _file.Close(); err != nil { 282 | return err 283 | } 284 | 285 | _file, err = os.OpenFile(file, os.O_WRONLY, os.ModeAppend) 286 | if err != nil { 287 | return err 288 | } 289 | 290 | return cr.Encrypt(buffer, _file) 291 | 292 | } 293 | 294 | const layout = "020106" 295 | 296 | func backup(file string) error { 297 | current := time.Now() 298 | newName := fmt.Sprintf("%s.%s-%d", file, current.Format(layout), current.Unix()) 299 | src, err := os.Open(file) 300 | if err != nil { 301 | return err 302 | } 303 | f, err := os.Create(newName) 304 | if err != nil { 305 | return err 306 | } 307 | _, err = io.Copy(f, src) 308 | return err 309 | } 310 | -------------------------------------------------------------------------------- /ezpwd_tui/colors.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/gdamore/tcell" 8 | ) 9 | 10 | type loginForm struct { 11 | Background tcell.Color 12 | Title tcell.Color 13 | Border tcell.Color 14 | Label tcell.Color 15 | ButtonBackground tcell.Color 16 | ButtonText tcell.Color 17 | FieldBackground tcell.Color 18 | FieldText tcell.Color 19 | } 20 | 21 | type passwordMgmtForm struct { 22 | Background tcell.Color 23 | TitleAdd tcell.Color 24 | TitleUpdate tcell.Color 25 | BorderAdd tcell.Color 26 | BorderUpdate tcell.Color 27 | Label tcell.Color 28 | ButtonBackground tcell.Color 29 | ButtonText tcell.Color 30 | FieldBackground tcell.Color 31 | FieldText tcell.Color 32 | } 33 | 34 | type confirmForm struct { 35 | Background tcell.Color 36 | Title tcell.Color 37 | Border tcell.Color 38 | Label tcell.Color 39 | ButtonBackground tcell.Color 40 | ButtonText tcell.Color 41 | FieldBackground tcell.Color 42 | FieldText tcell.Color 43 | } 44 | 45 | type globalScreen struct { 46 | Background tcell.Color 47 | Title tcell.Color 48 | Border tcell.Color 49 | HelpText tcell.Color 50 | } 51 | 52 | type passwordsTable struct { 53 | Background tcell.Color 54 | Title tcell.Color 55 | Border tcell.Color 56 | Label tcell.Color 57 | ButtonBackground tcell.Color 58 | ButtonText tcell.Color 59 | ButtonAccent tcell.Color 60 | FieldBackground tcell.Color 61 | FieldText tcell.Color 62 | Selection tcell.Color 63 | SelectionBackground tcell.Color 64 | Header tcell.Color 65 | CopiedBackground tcell.Color 66 | CopiedText tcell.Color 67 | CopiedTitle tcell.Color 68 | CopiedBorder tcell.Color 69 | } 70 | 71 | type messages struct { 72 | SuccessBackground tcell.Color 73 | SuccessBorder tcell.Color 74 | SuccessTitle tcell.Color 75 | SuccessText tcell.Color 76 | FailureBackground tcell.Color 77 | FailureBorder tcell.Color 78 | FailureTitle tcell.Color 79 | FailureText tcell.Color 80 | } 81 | 82 | type colorSchema struct { 83 | LoginFormColors loginForm 84 | PasswordMgmtColors passwordMgmtForm 85 | ConfirmFormColors confirmForm 86 | GlobalScreenColors globalScreen 87 | PasswordsTableColors passwordsTable 88 | MessagesColors messages 89 | } 90 | 91 | var ( 92 | BackgroundColorLight = tcell.NewRGBColor(0x87, 0x87, 0x5f) 93 | BackgroundColorDark = tcell.NewRGBColor(0x07, 0x36, 0x42) 94 | 95 | DefaultColorSchema colorSchema 96 | 97 | LightColorSchema = colorSchema{ 98 | LoginFormColors: loginForm{ 99 | Background: BackgroundColorLight, 100 | Title: tcell.ColorWhiteSmoke, 101 | Border: tcell.ColorGray, 102 | Label: tcell.ColorBlack, 103 | ButtonBackground: tcell.ColorDarkGray, 104 | ButtonText: tcell.ColorWhite, 105 | FieldBackground: tcell.ColorBeige, 106 | FieldText: tcell.ColorBlack, 107 | }, 108 | ConfirmFormColors: confirmForm{ 109 | Background: BackgroundColorLight, 110 | Title: tcell.ColorWhiteSmoke, 111 | Border: tcell.ColorGray, 112 | Label: tcell.ColorBlack, 113 | ButtonBackground: tcell.ColorDarkGray, 114 | ButtonText: tcell.ColorWhite, 115 | FieldBackground: tcell.ColorBeige, 116 | FieldText: tcell.ColorBlack, 117 | }, 118 | PasswordMgmtColors: passwordMgmtForm{ 119 | Background: BackgroundColorLight, 120 | TitleAdd: tcell.ColorWhiteSmoke, 121 | TitleUpdate: tcell.ColorMistyRose, 122 | BorderAdd: tcell.ColorGray, 123 | BorderUpdate: tcell.ColorMistyRose, 124 | Label: tcell.ColorBlack, 125 | ButtonBackground: tcell.ColorDarkGray, 126 | ButtonText: tcell.ColorWhite, 127 | FieldBackground: tcell.ColorBeige, 128 | FieldText: tcell.ColorBlack, 129 | }, 130 | GlobalScreenColors: globalScreen{ 131 | Background: BackgroundColorLight, 132 | Title: tcell.ColorNavajoWhite, 133 | Border: tcell.ColorGray, 134 | HelpText: tcell.ColorLightGray, 135 | }, 136 | PasswordsTableColors: passwordsTable{ 137 | Background: BackgroundColorLight, 138 | Title: tcell.ColorWhiteSmoke, 139 | Border: tcell.ColorGray, 140 | Label: tcell.ColorBlack, 141 | ButtonBackground: tcell.ColorDarkGray, 142 | ButtonText: tcell.ColorWhite, 143 | FieldBackground: tcell.ColorBeige, 144 | FieldText: tcell.ColorBlack, 145 | Selection: tcell.ColorGreen, 146 | SelectionBackground: tcell.ColorWheat, 147 | Header: tcell.ColorBisque, 148 | ButtonAccent: tcell.ColorBlueViolet, 149 | CopiedBackground: tcell.ColorGold, 150 | CopiedText: tcell.ColorBlack, 151 | CopiedTitle: tcell.ColorGreen, 152 | CopiedBorder: tcell.ColorWhiteSmoke, 153 | }, 154 | MessagesColors: messages{ 155 | SuccessBackground: BackgroundColorLight, 156 | SuccessTitle: tcell.ColorNavajoWhite, 157 | SuccessBorder: tcell.ColorGray, 158 | SuccessText: tcell.ColorGreen, 159 | FailureBackground: tcell.ColorBlack, 160 | FailureTitle: tcell.ColorRed, 161 | FailureBorder: tcell.ColorRed, 162 | FailureText: tcell.ColorOrangeRed, 163 | }, 164 | } 165 | DarkColorSchema = colorSchema{ 166 | LoginFormColors: loginForm{ 167 | Background: BackgroundColorDark, 168 | Title: tcell.ColorWhiteSmoke, 169 | Border: tcell.ColorGray, 170 | Label: tcell.ColorWheat, 171 | ButtonBackground: tcell.ColorDarkGray, 172 | ButtonText: tcell.ColorWhite, 173 | FieldBackground: tcell.ColorBeige, 174 | FieldText: tcell.ColorBlack, 175 | }, 176 | ConfirmFormColors: confirmForm{ 177 | Background: BackgroundColorDark, 178 | Title: tcell.ColorWhiteSmoke, 179 | Border: tcell.ColorGray, 180 | Label: tcell.ColorWhiteSmoke, 181 | ButtonBackground: tcell.ColorDarkGray, 182 | ButtonText: tcell.ColorWhite, 183 | FieldBackground: tcell.ColorBeige, 184 | FieldText: tcell.ColorBlack, 185 | }, 186 | PasswordMgmtColors: passwordMgmtForm{ 187 | Background: BackgroundColorDark, 188 | TitleAdd: tcell.ColorWhiteSmoke, 189 | TitleUpdate: tcell.ColorMistyRose, 190 | BorderAdd: tcell.ColorGray, 191 | BorderUpdate: tcell.ColorMistyRose, 192 | Label: tcell.ColorWhiteSmoke, 193 | ButtonBackground: tcell.ColorDarkGray, 194 | ButtonText: tcell.ColorWhite, 195 | FieldBackground: tcell.ColorBeige, 196 | FieldText: tcell.ColorBlack, 197 | }, 198 | GlobalScreenColors: globalScreen{ 199 | Background: BackgroundColorDark, 200 | Title: tcell.ColorNavajoWhite, 201 | Border: tcell.ColorGray, 202 | HelpText: tcell.ColorLightGray, 203 | }, 204 | PasswordsTableColors: passwordsTable{ 205 | Background: BackgroundColorDark, 206 | Title: tcell.ColorWhiteSmoke, 207 | Border: tcell.ColorGray, 208 | Label: tcell.ColorWhiteSmoke, 209 | ButtonBackground: tcell.ColorLightGray, 210 | ButtonText: tcell.ColorBlack, 211 | FieldBackground: tcell.ColorBeige, 212 | FieldText: tcell.ColorBlack, 213 | Selection: tcell.ColorGreen, 214 | SelectionBackground: tcell.ColorWheat, 215 | Header: tcell.ColorBisque, 216 | ButtonAccent: tcell.ColorBlueViolet, 217 | CopiedBackground: tcell.ColorGold, 218 | CopiedText: tcell.ColorBlack, 219 | CopiedTitle: tcell.ColorGreen, 220 | CopiedBorder: tcell.ColorWhiteSmoke, 221 | }, 222 | MessagesColors: messages{ 223 | SuccessBackground: BackgroundColorDark, 224 | SuccessTitle: tcell.ColorNavajoWhite, 225 | SuccessBorder: tcell.ColorGray, 226 | SuccessText: tcell.ColorGreen, 227 | FailureBackground: tcell.ColorBlack, 228 | FailureTitle: tcell.ColorRed, 229 | FailureBorder: tcell.ColorRed, 230 | FailureText: tcell.ColorOrangeRed, 231 | }, 232 | } 233 | ) 234 | 235 | func color2Hex(c tcell.Color) string { 236 | r, g, b := c.RGB() 237 | return fmt.Sprintf("#%02x%02x%02x", r, g, b) 238 | } 239 | 240 | func hex2Color(src string) tcell.Color { 241 | x, err := strconv.ParseInt(src[1:], 16, 32) 242 | if err != nil { 243 | panic(err) 244 | } 245 | return tcell.NewHexColor(int32(x)) 246 | } 247 | -------------------------------------------------------------------------------- /ezpwd_tui/colors_easyjson.go: -------------------------------------------------------------------------------- 1 | // Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT. 2 | 3 | package main 4 | 5 | import ( 6 | json "encoding/json" 7 | easyjson "github.com/mailru/easyjson" 8 | jlexer "github.com/mailru/easyjson/jlexer" 9 | jwriter "github.com/mailru/easyjson/jwriter" 10 | ) 11 | 12 | // suppress unused package warning 13 | var ( 14 | _ *json.RawMessage 15 | _ *jlexer.Lexer 16 | _ *jwriter.Writer 17 | _ easyjson.Marshaler 18 | ) 19 | 20 | func easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo(in *jlexer.Lexer, out *passwordsTable) { 21 | isTopLevel := in.IsStart() 22 | if in.IsNull() { 23 | if isTopLevel { 24 | in.Consumed() 25 | } 26 | in.Skip() 27 | return 28 | } 29 | in.Delim('{') 30 | for !in.IsDelim('}') { 31 | key := in.UnsafeString() 32 | in.WantColon() 33 | if in.IsNull() { 34 | in.Skip() 35 | in.WantComma() 36 | continue 37 | } 38 | switch key { 39 | case "Background": 40 | out.Background = hex2Color(in.String()) 41 | case "Title": 42 | out.Title = hex2Color(in.String()) 43 | case "Border": 44 | out.Border = hex2Color(in.String()) 45 | case "Label": 46 | out.Label = hex2Color(in.String()) 47 | case "ButtonBackground": 48 | out.ButtonBackground = hex2Color(in.String()) 49 | case "ButtonText": 50 | out.ButtonText = hex2Color(in.String()) 51 | case "ButtonAccent": 52 | out.ButtonAccent = hex2Color(in.String()) 53 | case "FieldBackground": 54 | out.FieldBackground = hex2Color(in.String()) 55 | case "FieldText": 56 | out.FieldText = hex2Color(in.String()) 57 | case "Selection": 58 | out.Selection = hex2Color(in.String()) 59 | case "SelectionBackground": 60 | out.SelectionBackground = hex2Color(in.String()) 61 | case "Header": 62 | out.Header = hex2Color(in.String()) 63 | case "CopiedBackground": 64 | out.CopiedBackground = hex2Color(in.String()) 65 | case "CopiedText": 66 | out.CopiedText = hex2Color(in.String()) 67 | case "CopiedTitle": 68 | out.CopiedTitle = hex2Color(in.String()) 69 | case "CopiedBorder": 70 | out.CopiedBorder = hex2Color(in.String()) 71 | default: 72 | in.SkipRecursive() 73 | } 74 | in.WantComma() 75 | } 76 | in.Delim('}') 77 | if isTopLevel { 78 | in.Consumed() 79 | } 80 | } 81 | func easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo(out *jwriter.Writer, in passwordsTable) { 82 | out.RawByte('{') 83 | first := true 84 | _ = first 85 | { 86 | const prefix string = ",\"Background\":" 87 | out.RawString(prefix[1:]) 88 | out.String(color2Hex(in.Background)) 89 | } 90 | { 91 | const prefix string = ",\"Title\":" 92 | out.RawString(prefix) 93 | out.String(color2Hex(in.Title)) 94 | } 95 | { 96 | const prefix string = ",\"Border\":" 97 | out.RawString(prefix) 98 | out.String(color2Hex(in.Border)) 99 | } 100 | { 101 | const prefix string = ",\"Label\":" 102 | out.RawString(prefix) 103 | out.String(color2Hex(in.Label)) 104 | } 105 | { 106 | const prefix string = ",\"ButtonBackground\":" 107 | out.RawString(prefix) 108 | out.String(color2Hex(in.ButtonBackground)) 109 | } 110 | { 111 | const prefix string = ",\"ButtonText\":" 112 | out.RawString(prefix) 113 | out.String(color2Hex(in.ButtonText)) 114 | } 115 | { 116 | const prefix string = ",\"ButtonAccent\":" 117 | out.RawString(prefix) 118 | out.String(color2Hex(in.ButtonAccent)) 119 | } 120 | { 121 | const prefix string = ",\"FieldBackground\":" 122 | out.RawString(prefix) 123 | out.String(color2Hex(in.FieldBackground)) 124 | } 125 | { 126 | const prefix string = ",\"FieldText\":" 127 | out.RawString(prefix) 128 | out.String(color2Hex(in.FieldText)) 129 | } 130 | { 131 | const prefix string = ",\"Selection\":" 132 | out.RawString(prefix) 133 | out.String(color2Hex(in.Selection)) 134 | } 135 | { 136 | const prefix string = ",\"SelectionBackground\":" 137 | out.RawString(prefix) 138 | out.String(color2Hex(in.SelectionBackground)) 139 | } 140 | { 141 | const prefix string = ",\"Header\":" 142 | out.RawString(prefix) 143 | out.String(color2Hex(in.Header)) 144 | } 145 | { 146 | const prefix string = ",\"CopiedBackground\":" 147 | out.RawString(prefix) 148 | out.String(color2Hex(in.CopiedBackground)) 149 | } 150 | { 151 | const prefix string = ",\"CopiedText\":" 152 | out.RawString(prefix) 153 | out.String(color2Hex(in.CopiedText)) 154 | } 155 | { 156 | const prefix string = ",\"CopiedTitle\":" 157 | out.RawString(prefix) 158 | out.String(color2Hex(in.CopiedTitle)) 159 | } 160 | { 161 | const prefix string = ",\"CopiedBorder\":" 162 | out.RawString(prefix) 163 | out.String(color2Hex(in.CopiedBorder)) 164 | } 165 | out.RawByte('}') 166 | } 167 | 168 | // MarshalEasyJSON supports easyjson.Marshaler interface 169 | func (v passwordsTable) MarshalEasyJSON(w *jwriter.Writer) { 170 | easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo(w, v) 171 | } 172 | 173 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 174 | func (v *passwordsTable) UnmarshalEasyJSON(l *jlexer.Lexer) { 175 | easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo(l, v) 176 | } 177 | func easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo1(in *jlexer.Lexer, out *passwordMgmtForm) { 178 | isTopLevel := in.IsStart() 179 | if in.IsNull() { 180 | if isTopLevel { 181 | in.Consumed() 182 | } 183 | in.Skip() 184 | return 185 | } 186 | in.Delim('{') 187 | for !in.IsDelim('}') { 188 | key := in.UnsafeString() 189 | in.WantColon() 190 | if in.IsNull() { 191 | in.Skip() 192 | in.WantComma() 193 | continue 194 | } 195 | switch key { 196 | case "Background": 197 | out.Background = hex2Color(in.String()) 198 | case "TitleAdd": 199 | out.TitleAdd = hex2Color(in.String()) 200 | case "TitleUpdate": 201 | out.TitleUpdate = hex2Color(in.String()) 202 | case "BorderAdd": 203 | out.BorderAdd = hex2Color(in.String()) 204 | case "BorderUpdate": 205 | out.BorderUpdate = hex2Color(in.String()) 206 | case "Label": 207 | out.Label = hex2Color(in.String()) 208 | case "ButtonBackground": 209 | out.ButtonBackground = hex2Color(in.String()) 210 | case "ButtonText": 211 | out.ButtonText = hex2Color(in.String()) 212 | case "FieldBackground": 213 | out.FieldBackground = hex2Color(in.String()) 214 | case "FieldText": 215 | out.FieldText = hex2Color(in.String()) 216 | default: 217 | in.SkipRecursive() 218 | } 219 | in.WantComma() 220 | } 221 | in.Delim('}') 222 | if isTopLevel { 223 | in.Consumed() 224 | } 225 | } 226 | func easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo1(out *jwriter.Writer, in passwordMgmtForm) { 227 | out.RawByte('{') 228 | first := true 229 | _ = first 230 | { 231 | const prefix string = ",\"Background\":" 232 | out.RawString(prefix[1:]) 233 | out.String(color2Hex(in.Background)) 234 | } 235 | { 236 | const prefix string = ",\"TitleAdd\":" 237 | out.RawString(prefix) 238 | out.String(color2Hex(in.TitleAdd)) 239 | } 240 | { 241 | const prefix string = ",\"TitleUpdate\":" 242 | out.RawString(prefix) 243 | out.String(color2Hex(in.TitleUpdate)) 244 | } 245 | { 246 | const prefix string = ",\"BorderAdd\":" 247 | out.RawString(prefix) 248 | out.String(color2Hex(in.BorderAdd)) 249 | } 250 | { 251 | const prefix string = ",\"BorderUpdate\":" 252 | out.RawString(prefix) 253 | out.String(color2Hex(in.BorderUpdate)) 254 | } 255 | { 256 | const prefix string = ",\"Label\":" 257 | out.RawString(prefix) 258 | out.String(color2Hex(in.Label)) 259 | } 260 | { 261 | const prefix string = ",\"ButtonBackground\":" 262 | out.RawString(prefix) 263 | out.String(color2Hex(in.ButtonBackground)) 264 | } 265 | { 266 | const prefix string = ",\"ButtonText\":" 267 | out.RawString(prefix) 268 | out.String(color2Hex(in.ButtonText)) 269 | } 270 | { 271 | const prefix string = ",\"FieldBackground\":" 272 | out.RawString(prefix) 273 | out.String(color2Hex(in.FieldBackground)) 274 | } 275 | { 276 | const prefix string = ",\"FieldText\":" 277 | out.RawString(prefix) 278 | out.String(color2Hex(in.FieldText)) 279 | } 280 | out.RawByte('}') 281 | } 282 | 283 | // MarshalEasyJSON supports easyjson.Marshaler interface 284 | func (v passwordMgmtForm) MarshalEasyJSON(w *jwriter.Writer) { 285 | easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo1(w, v) 286 | } 287 | 288 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 289 | func (v *passwordMgmtForm) UnmarshalEasyJSON(l *jlexer.Lexer) { 290 | easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo1(l, v) 291 | } 292 | func easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo2(in *jlexer.Lexer, out *messages) { 293 | isTopLevel := in.IsStart() 294 | if in.IsNull() { 295 | if isTopLevel { 296 | in.Consumed() 297 | } 298 | in.Skip() 299 | return 300 | } 301 | in.Delim('{') 302 | for !in.IsDelim('}') { 303 | key := in.UnsafeString() 304 | in.WantColon() 305 | if in.IsNull() { 306 | in.Skip() 307 | in.WantComma() 308 | continue 309 | } 310 | switch key { 311 | case "SuccessBackground": 312 | out.SuccessBackground = hex2Color(in.String()) 313 | case "SuccessBorder": 314 | out.SuccessBorder = hex2Color(in.String()) 315 | case "SuccessTitle": 316 | out.SuccessTitle = hex2Color(in.String()) 317 | case "SuccessText": 318 | out.SuccessText = hex2Color(in.String()) 319 | case "FailureBackground": 320 | out.FailureBackground = hex2Color(in.String()) 321 | case "FailureBorder": 322 | out.FailureBorder = hex2Color(in.String()) 323 | case "FailureTitle": 324 | out.FailureTitle = hex2Color(in.String()) 325 | case "FailureText": 326 | out.FailureText = hex2Color(in.String()) 327 | default: 328 | in.SkipRecursive() 329 | } 330 | in.WantComma() 331 | } 332 | in.Delim('}') 333 | if isTopLevel { 334 | in.Consumed() 335 | } 336 | } 337 | func easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo2(out *jwriter.Writer, in messages) { 338 | out.RawByte('{') 339 | first := true 340 | _ = first 341 | { 342 | const prefix string = ",\"SuccessBackground\":" 343 | out.RawString(prefix[1:]) 344 | out.String(color2Hex(in.SuccessBackground)) 345 | } 346 | { 347 | const prefix string = ",\"SuccessBorder\":" 348 | out.RawString(prefix) 349 | out.String(color2Hex(in.SuccessBorder)) 350 | } 351 | { 352 | const prefix string = ",\"SuccessTitle\":" 353 | out.RawString(prefix) 354 | out.String(color2Hex(in.SuccessTitle)) 355 | } 356 | { 357 | const prefix string = ",\"SuccessText\":" 358 | out.RawString(prefix) 359 | out.String(color2Hex(in.SuccessText)) 360 | } 361 | { 362 | const prefix string = ",\"FailureBackground\":" 363 | out.RawString(prefix) 364 | out.String(color2Hex(in.FailureBackground)) 365 | } 366 | { 367 | const prefix string = ",\"FailureBorder\":" 368 | out.RawString(prefix) 369 | out.String(color2Hex(in.FailureBorder)) 370 | } 371 | { 372 | const prefix string = ",\"FailureTitle\":" 373 | out.RawString(prefix) 374 | out.String(color2Hex(in.FailureTitle)) 375 | } 376 | { 377 | const prefix string = ",\"FailureText\":" 378 | out.RawString(prefix) 379 | out.String(color2Hex(in.FailureText)) 380 | } 381 | out.RawByte('}') 382 | } 383 | 384 | // MarshalEasyJSON supports easyjson.Marshaler interface 385 | func (v messages) MarshalEasyJSON(w *jwriter.Writer) { 386 | easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo2(w, v) 387 | } 388 | 389 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 390 | func (v *messages) UnmarshalEasyJSON(l *jlexer.Lexer) { 391 | easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo2(l, v) 392 | } 393 | func easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo3(in *jlexer.Lexer, out *loginForm) { 394 | isTopLevel := in.IsStart() 395 | if in.IsNull() { 396 | if isTopLevel { 397 | in.Consumed() 398 | } 399 | in.Skip() 400 | return 401 | } 402 | in.Delim('{') 403 | for !in.IsDelim('}') { 404 | key := in.UnsafeString() 405 | in.WantColon() 406 | if in.IsNull() { 407 | in.Skip() 408 | in.WantComma() 409 | continue 410 | } 411 | switch key { 412 | case "Background": 413 | out.Background = hex2Color(in.String()) 414 | case "Title": 415 | out.Title = hex2Color(in.String()) 416 | case "Border": 417 | out.Border = hex2Color(in.String()) 418 | case "Label": 419 | out.Label = hex2Color(in.String()) 420 | case "ButtonBackground": 421 | out.ButtonBackground = hex2Color(in.String()) 422 | case "ButtonText": 423 | out.ButtonText = hex2Color(in.String()) 424 | case "FieldBackground": 425 | out.FieldBackground = hex2Color(in.String()) 426 | case "FieldText": 427 | out.FieldText = hex2Color(in.String()) 428 | default: 429 | in.SkipRecursive() 430 | } 431 | in.WantComma() 432 | } 433 | in.Delim('}') 434 | if isTopLevel { 435 | in.Consumed() 436 | } 437 | } 438 | func easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo3(out *jwriter.Writer, in loginForm) { 439 | out.RawByte('{') 440 | first := true 441 | _ = first 442 | { 443 | const prefix string = ",\"Background\":" 444 | out.RawString(prefix[1:]) 445 | out.String(color2Hex(in.Background)) 446 | } 447 | { 448 | const prefix string = ",\"Title\":" 449 | out.RawString(prefix) 450 | out.String(color2Hex(in.Title)) 451 | } 452 | { 453 | const prefix string = ",\"Border\":" 454 | out.RawString(prefix) 455 | out.String(color2Hex(in.Border)) 456 | } 457 | { 458 | const prefix string = ",\"Label\":" 459 | out.RawString(prefix) 460 | out.String(color2Hex(in.Label)) 461 | } 462 | { 463 | const prefix string = ",\"ButtonBackground\":" 464 | out.RawString(prefix) 465 | out.String(color2Hex(in.ButtonBackground)) 466 | } 467 | { 468 | const prefix string = ",\"ButtonText\":" 469 | out.RawString(prefix) 470 | out.String(color2Hex(in.ButtonText)) 471 | } 472 | { 473 | const prefix string = ",\"FieldBackground\":" 474 | out.RawString(prefix) 475 | out.String(color2Hex(in.FieldBackground)) 476 | } 477 | { 478 | const prefix string = ",\"FieldText\":" 479 | out.RawString(prefix) 480 | out.String(color2Hex(in.FieldText)) 481 | } 482 | out.RawByte('}') 483 | } 484 | 485 | // MarshalEasyJSON supports easyjson.Marshaler interface 486 | func (v loginForm) MarshalEasyJSON(w *jwriter.Writer) { 487 | easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo3(w, v) 488 | } 489 | 490 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 491 | func (v *loginForm) UnmarshalEasyJSON(l *jlexer.Lexer) { 492 | easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo3(l, v) 493 | } 494 | func easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo4(in *jlexer.Lexer, out *globalScreen) { 495 | isTopLevel := in.IsStart() 496 | if in.IsNull() { 497 | if isTopLevel { 498 | in.Consumed() 499 | } 500 | in.Skip() 501 | return 502 | } 503 | in.Delim('{') 504 | for !in.IsDelim('}') { 505 | key := in.UnsafeString() 506 | in.WantColon() 507 | if in.IsNull() { 508 | in.Skip() 509 | in.WantComma() 510 | continue 511 | } 512 | switch key { 513 | case "Background": 514 | out.Background = hex2Color(in.String()) 515 | case "Title": 516 | out.Title = hex2Color(in.String()) 517 | case "Border": 518 | out.Border = hex2Color(in.String()) 519 | case "HelpText": 520 | out.HelpText = hex2Color(in.String()) 521 | default: 522 | in.SkipRecursive() 523 | } 524 | in.WantComma() 525 | } 526 | in.Delim('}') 527 | if isTopLevel { 528 | in.Consumed() 529 | } 530 | } 531 | func easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo4(out *jwriter.Writer, in globalScreen) { 532 | out.RawByte('{') 533 | first := true 534 | _ = first 535 | { 536 | const prefix string = ",\"Background\":" 537 | out.RawString(prefix[1:]) 538 | out.String(color2Hex(in.Background)) 539 | } 540 | { 541 | const prefix string = ",\"Title\":" 542 | out.RawString(prefix) 543 | out.String(color2Hex(in.Title)) 544 | } 545 | { 546 | const prefix string = ",\"Border\":" 547 | out.RawString(prefix) 548 | out.String(color2Hex(in.Border)) 549 | } 550 | { 551 | const prefix string = ",\"HelpText\":" 552 | out.RawString(prefix) 553 | out.String(color2Hex(in.HelpText)) 554 | } 555 | out.RawByte('}') 556 | } 557 | 558 | // MarshalEasyJSON supports easyjson.Marshaler interface 559 | func (v globalScreen) MarshalEasyJSON(w *jwriter.Writer) { 560 | easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo4(w, v) 561 | } 562 | 563 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 564 | func (v *globalScreen) UnmarshalEasyJSON(l *jlexer.Lexer) { 565 | easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo4(l, v) 566 | } 567 | func easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo5(in *jlexer.Lexer, out *confirmForm) { 568 | isTopLevel := in.IsStart() 569 | if in.IsNull() { 570 | if isTopLevel { 571 | in.Consumed() 572 | } 573 | in.Skip() 574 | return 575 | } 576 | in.Delim('{') 577 | for !in.IsDelim('}') { 578 | key := in.UnsafeString() 579 | in.WantColon() 580 | if in.IsNull() { 581 | in.Skip() 582 | in.WantComma() 583 | continue 584 | } 585 | switch key { 586 | case "Background": 587 | out.Background = hex2Color(in.String()) 588 | case "Title": 589 | out.Title = hex2Color(in.String()) 590 | case "Border": 591 | out.Border = hex2Color(in.String()) 592 | case "Label": 593 | out.Label = hex2Color(in.String()) 594 | case "ButtonBackground": 595 | out.ButtonBackground = hex2Color(in.String()) 596 | case "ButtonText": 597 | out.ButtonText = hex2Color(in.String()) 598 | case "FieldBackground": 599 | out.FieldBackground = hex2Color(in.String()) 600 | case "FieldText": 601 | out.FieldText = hex2Color(in.String()) 602 | default: 603 | in.SkipRecursive() 604 | } 605 | in.WantComma() 606 | } 607 | in.Delim('}') 608 | if isTopLevel { 609 | in.Consumed() 610 | } 611 | } 612 | func easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo5(out *jwriter.Writer, in confirmForm) { 613 | out.RawByte('{') 614 | first := true 615 | _ = first 616 | { 617 | const prefix string = ",\"Background\":" 618 | out.RawString(prefix[1:]) 619 | out.String(color2Hex(in.Background)) 620 | } 621 | { 622 | const prefix string = ",\"Title\":" 623 | out.RawString(prefix) 624 | out.String(color2Hex(in.Title)) 625 | } 626 | { 627 | const prefix string = ",\"Border\":" 628 | out.RawString(prefix) 629 | out.String(color2Hex(in.Border)) 630 | } 631 | { 632 | const prefix string = ",\"Label\":" 633 | out.RawString(prefix) 634 | out.String(color2Hex(in.Label)) 635 | } 636 | { 637 | const prefix string = ",\"ButtonBackground\":" 638 | out.RawString(prefix) 639 | out.String(color2Hex(in.ButtonBackground)) 640 | } 641 | { 642 | const prefix string = ",\"ButtonText\":" 643 | out.RawString(prefix) 644 | out.String(color2Hex(in.ButtonText)) 645 | } 646 | { 647 | const prefix string = ",\"FieldBackground\":" 648 | out.RawString(prefix) 649 | out.String(color2Hex(in.FieldBackground)) 650 | } 651 | { 652 | const prefix string = ",\"FieldText\":" 653 | out.RawString(prefix) 654 | out.String(color2Hex(in.FieldText)) 655 | } 656 | out.RawByte('}') 657 | } 658 | 659 | // MarshalEasyJSON supports easyjson.Marshaler interface 660 | func (v confirmForm) MarshalEasyJSON(w *jwriter.Writer) { 661 | easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo5(w, v) 662 | } 663 | 664 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 665 | func (v *confirmForm) UnmarshalEasyJSON(l *jlexer.Lexer) { 666 | easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo5(l, v) 667 | } 668 | func easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo6(in *jlexer.Lexer, out *colorSchema) { 669 | isTopLevel := in.IsStart() 670 | if in.IsNull() { 671 | if isTopLevel { 672 | in.Consumed() 673 | } 674 | in.Skip() 675 | return 676 | } 677 | in.Delim('{') 678 | for !in.IsDelim('}') { 679 | key := in.UnsafeString() 680 | in.WantColon() 681 | if in.IsNull() { 682 | in.Skip() 683 | in.WantComma() 684 | continue 685 | } 686 | switch key { 687 | case "LoginFormColors": 688 | (out.LoginFormColors).UnmarshalEasyJSON(in) 689 | case "PasswordMgmtColors": 690 | (out.PasswordMgmtColors).UnmarshalEasyJSON(in) 691 | case "ConfirmFormColors": 692 | (out.ConfirmFormColors).UnmarshalEasyJSON(in) 693 | case "GlobalScreenColors": 694 | (out.GlobalScreenColors).UnmarshalEasyJSON(in) 695 | case "PasswordsTableColors": 696 | (out.PasswordsTableColors).UnmarshalEasyJSON(in) 697 | case "MessagesColors": 698 | (out.MessagesColors).UnmarshalEasyJSON(in) 699 | default: 700 | in.SkipRecursive() 701 | } 702 | in.WantComma() 703 | } 704 | in.Delim('}') 705 | if isTopLevel { 706 | in.Consumed() 707 | } 708 | } 709 | func easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo6(out *jwriter.Writer, in colorSchema) { 710 | out.RawByte('{') 711 | first := true 712 | _ = first 713 | { 714 | const prefix string = ",\"LoginFormColors\":" 715 | out.RawString(prefix[1:]) 716 | (in.LoginFormColors).MarshalEasyJSON(out) 717 | } 718 | { 719 | const prefix string = ",\"PasswordMgmtColors\":" 720 | out.RawString(prefix) 721 | (in.PasswordMgmtColors).MarshalEasyJSON(out) 722 | } 723 | { 724 | const prefix string = ",\"ConfirmFormColors\":" 725 | out.RawString(prefix) 726 | (in.ConfirmFormColors).MarshalEasyJSON(out) 727 | } 728 | { 729 | const prefix string = ",\"GlobalScreenColors\":" 730 | out.RawString(prefix) 731 | (in.GlobalScreenColors).MarshalEasyJSON(out) 732 | } 733 | { 734 | const prefix string = ",\"PasswordsTableColors\":" 735 | out.RawString(prefix) 736 | (in.PasswordsTableColors).MarshalEasyJSON(out) 737 | } 738 | { 739 | const prefix string = ",\"MessagesColors\":" 740 | out.RawString(prefix) 741 | (in.MessagesColors).MarshalEasyJSON(out) 742 | } 743 | out.RawByte('}') 744 | } 745 | 746 | // MarshalEasyJSON supports easyjson.Marshaler interface 747 | func (v colorSchema) MarshalEasyJSON(w *jwriter.Writer) { 748 | easyjson525b21b0EncodeGithubComJdevelopEzpwdDemo6(w, v) 749 | } 750 | 751 | // UnmarshalEasyJSON supports easyjson.Unmarshaler interface 752 | func (v *colorSchema) UnmarshalEasyJSON(l *jlexer.Lexer) { 753 | easyjson525b21b0DecodeGithubComJdevelopEzpwdDemo6(l, v) 754 | } 755 | -------------------------------------------------------------------------------- /ezpwd_tui/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | easyjson "github.com/mailru/easyjson" 7 | "log" 8 | "os" 9 | "os/user" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/gdamore/tcell" 14 | "github.com/jdevelop/ezpwd" 15 | "github.com/rivo/tview" 16 | ) 17 | 18 | const ( 19 | screenPwd = "password" 20 | screenPwds = "passwordsTable" 21 | screenError = "errors" 22 | screenPwdCopied = "copied" 23 | screenPwdManage = "manage" 24 | screenConfirm = "confirm" 25 | ) 26 | 27 | var ( 28 | passFile = flag.String("passfile", "private/pass.enc", "Password file") 29 | schemaFile = flag.String("schema", "", "Color schema file") 30 | dumpSchema = flag.Bool("dump-schema", false, "Print current schema and exit") 31 | darkSchema = flag.Bool("dark", false, "Dark colors") 32 | ) 33 | 34 | type devEzpwd struct { 35 | app *tview.Application 36 | screen tcell.Screen 37 | passwordPath string 38 | passwordsChan chan []ezpwd.Password 39 | pages *tview.Pages 40 | crypto *ezpwd.Crypto 41 | } 42 | 43 | func NewEzpwd(passwordsPath string) (*devEzpwd, error) { 44 | screen, err := tcell.NewScreen() 45 | if err != nil { 46 | return nil, err 47 | } 48 | if err := screen.Init(); err != nil { 49 | return nil, err 50 | } 51 | app := tview.NewApplication() 52 | app.SetScreen(screen) 53 | 54 | var instance = devEzpwd{ 55 | app: app, 56 | screen: screen, 57 | passwordPath: passwordsPath, 58 | pages: tview.NewPages(), 59 | } 60 | 61 | return &instance, nil 62 | } 63 | 64 | func (e *devEzpwd) Run() error { 65 | e.passwordsChan = make(chan []ezpwd.Password) 66 | var ( 67 | form *tview.Form 68 | height int 69 | ) 70 | _, err := os.Stat(e.passwordPath) 71 | switch { 72 | case err == nil: 73 | form, height = e.passwordForm(), 7 74 | case errors.Is(err, os.ErrNotExist): 75 | form, height = e.initForm(), 9 76 | default: 77 | log.Fatalf("can't use storage at path %s: %+v", e.passwordPath, err) 78 | } 79 | e.passwordsTable() 80 | e.pages. 81 | AddPage(screenPwd, modal(form, 40, height, func(p *tview.Box) { 82 | p.SetBackgroundColor(DefaultColorSchema.GlobalScreenColors.Background) 83 | p.SetBorderColor(DefaultColorSchema.GlobalScreenColors.Border) 84 | p.SetTitleColor(DefaultColorSchema.GlobalScreenColors.Title) 85 | }), true, true) 86 | frame := tview.NewFrame(e.pages) 87 | frame.SetBorder(true) 88 | frame.SetBackgroundColor(DefaultColorSchema.GlobalScreenColors.Background) 89 | frame.SetBorderColor(DefaultColorSchema.GlobalScreenColors.Border) 90 | frame.SetTitleColor(DefaultColorSchema.GlobalScreenColors.Title) 91 | frame.AddText("`Esc` to exit dialogs without saving", false, tview.AlignRight, DefaultColorSchema.GlobalScreenColors.HelpText) 92 | frame.AddText("`Ctrl-C` to quit application", false, tview.AlignRight, DefaultColorSchema.GlobalScreenColors.HelpText) 93 | frame.SetTitle("Storage " + e.passwordPath) 94 | e.app.SetRoot(frame, true).SetFocus(form) 95 | return e.app.Run() 96 | } 97 | 98 | func main() { 99 | u, err := user.Current() 100 | if err != nil { 101 | log.Fatal("can't retrieve current user", err) 102 | } 103 | flag.Parse() 104 | var encPath string 105 | if !strings.HasPrefix(*passFile, "/") { 106 | encPath = filepath.Join(u.HomeDir, *passFile) 107 | } else { 108 | encPath = *passFile 109 | } 110 | 111 | dir := filepath.Dir(encPath) 112 | 113 | switch d, err := os.Stat(dir); err { 114 | case nil: 115 | if !d.IsDir() { 116 | log.Fatalf("Can't use folder '%s'", dir) 117 | } 118 | case os.ErrNotExist: 119 | if err := os.MkdirAll(dir, 0700); err != nil { 120 | log.Fatalf("Can't create folder '%s'", dir) 121 | } 122 | default: 123 | log.Fatalf("Fatal error, aborting: %+v", err) 124 | } 125 | 126 | switch { 127 | case *schemaFile != "": 128 | f, err := os.Open(*schemaFile) 129 | if err != nil { 130 | log.Fatalf("Can't open schema file %s: %+v", *schemaFile, err) 131 | } 132 | if err := easyjson.UnmarshalFromReader(f, &DefaultColorSchema); err != nil { 133 | log.Fatalf("Can't read JSON from %s: %+v", *schemaFile, err) 134 | } 135 | case *darkSchema: 136 | DefaultColorSchema = DarkColorSchema 137 | default: 138 | DefaultColorSchema = LightColorSchema 139 | } 140 | 141 | if *dumpSchema { 142 | if _, err := easyjson.MarshalToWriter(&DefaultColorSchema, os.Stdout); err != nil { 143 | log.Fatalf("Can't encode JSON: %+v", err) 144 | } 145 | os.Exit(0) 146 | } 147 | 148 | p, err := NewEzpwd(encPath) 149 | if err != nil { 150 | log.Fatal(err) 151 | } 152 | 153 | if err := p.Run(); err != nil { 154 | log.Fatal(err) 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /ezpwd_tui/package.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "time" 8 | 9 | "github.com/gdamore/tcell" 10 | "github.com/rivo/tview" 11 | ) 12 | 13 | func modal(p tview.Primitive, width, height int, style ...func(p *tview.Box)) tview.Primitive { 14 | flex := tview.NewFlex(). 15 | AddItem(nil, 0, 1, false). 16 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 17 | AddItem(nil, 0, 1, false). 18 | AddItem(p, height, 1, true). 19 | AddItem(nil, 0, 1, false), width, 1, true). 20 | AddItem(nil, 0, 1, false) 21 | for _, f := range style { 22 | f(flex.Box) 23 | } 24 | return flex 25 | } 26 | 27 | func (e *devEzpwd) showMessage(title, msg, previousScreen string, styleUpd ...func(*tview.TextView)) { 28 | e.app.QueueUpdateDraw(func() { 29 | text := tview.NewTextView(). 30 | SetText(msg). 31 | SetWrap(true). 32 | SetTextAlign(tview.AlignCenter) 33 | text. 34 | SetTitle(title). 35 | SetBorder(true). 36 | SetBorderPadding(1, 1, 1, 1) 37 | for _, f := range styleUpd { 38 | f(text) 39 | } 40 | flex := tview.NewFlex(). 41 | SetDirection(tview.FlexRow). 42 | AddItem(nil, 0, 1, false). 43 | AddItem( 44 | tview.NewFlex(). 45 | AddItem(nil, 0, 1, false). 46 | AddItem(text, len(msg)+6, 0, true). 47 | AddItem(nil, 0, 1, false), 48 | 5, 0, true, 49 | ). 50 | AddItem(nil, 0, 1, false) 51 | e.pages.AddPage(screenError, flex, true, true) 52 | text.SetDoneFunc(func(k tcell.Key) { 53 | switch k { 54 | case tcell.KeyEnter, tcell.KeyEsc: 55 | e.pages.RemovePage(screenError) 56 | e.pages.SwitchToPage(previousScreen) 57 | } 58 | }) 59 | e.app.SetFocus(text) 60 | }) 61 | } 62 | 63 | func (e *devEzpwd) confirm(msg, from string, ok func()) { 64 | form := tview.NewForm().SetButtonsAlign(tview.AlignCenter) 65 | form.SetTitle(msg) 66 | form.SetBorder(true) 67 | form.SetBackgroundColor(DefaultColorSchema.ConfirmFormColors.Background) 68 | form.SetLabelColor(DefaultColorSchema.ConfirmFormColors.Label) 69 | form.SetButtonBackgroundColor(DefaultColorSchema.ConfirmFormColors.ButtonBackground) 70 | form.SetButtonTextColor(DefaultColorSchema.ConfirmFormColors.ButtonText) 71 | form.SetFieldBackgroundColor(DefaultColorSchema.ConfirmFormColors.FieldBackground) 72 | form.SetFieldTextColor(DefaultColorSchema.ConfirmFormColors.FieldText) 73 | cancelFunc := func() { 74 | e.pages.RemovePage(screenConfirm) 75 | e.pages.ShowPage(from) 76 | } 77 | form.SetCancelFunc(cancelFunc) 78 | form.AddButton("Ok", func() { 79 | ok() 80 | e.pages.RemovePage(screenConfirm) 81 | e.pages.ShowPage(from) 82 | }).AddButton("Cancel", cancelFunc) 83 | e.pages.AddPage(screenConfirm, modal(form, len(msg)+4, 5), true, true) 84 | e.app.SetFocus(form) 85 | } 86 | 87 | const layout = "020106" 88 | 89 | func backup(file string) error { 90 | current := time.Now() 91 | newName := fmt.Sprintf("%s.%s-%d", file, current.Format(layout), current.Unix()) 92 | src, err := os.Open(file) 93 | if err != nil { 94 | return err 95 | } 96 | f, err := os.Create(newName) 97 | if err != nil { 98 | return err 99 | } 100 | _, err = io.Copy(f, src) 101 | return err 102 | } 103 | 104 | func successMessageStyle(text *tview.TextView) { 105 | text.SetBorderColor(DefaultColorSchema.MessagesColors.SuccessBorder) 106 | text.SetTitleColor(DefaultColorSchema.MessagesColors.SuccessTitle) 107 | text.SetBackgroundColor(DefaultColorSchema.MessagesColors.SuccessBackground) 108 | text.SetTextColor(DefaultColorSchema.MessagesColors.SuccessText) 109 | } 110 | 111 | func errorsMessageStyle(text *tview.TextView) { 112 | text.SetBorderColor(DefaultColorSchema.MessagesColors.FailureBorder) 113 | text.SetTitleColor(DefaultColorSchema.MessagesColors.FailureTitle) 114 | text.SetBackgroundColor(DefaultColorSchema.MessagesColors.FailureBackground) 115 | text.SetTextColor(DefaultColorSchema.MessagesColors.FailureText) 116 | } 117 | -------------------------------------------------------------------------------- /ezpwd_tui/password_mgmt_form.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | 7 | "github.com/dchest/uniuri" 8 | "github.com/gdamore/tcell" 9 | "github.com/jdevelop/ezpwd" 10 | "github.com/rivo/tview" 11 | ) 12 | 13 | func (e *devEzpwd) passwordMgmtForm(id int, pwds []ezpwd.Password) *tview.Form { 14 | var genPwdFunc func() 15 | form := tview.NewForm().SetButtonsAlign(tview.AlignCenter) 16 | form.SetBackgroundColor(DefaultColorSchema.PasswordMgmtColors.Background) 17 | form.SetLabelColor(DefaultColorSchema.PasswordMgmtColors.Label) 18 | form.SetButtonBackgroundColor(DefaultColorSchema.PasswordMgmtColors.ButtonBackground) 19 | form.SetButtonTextColor(DefaultColorSchema.PasswordMgmtColors.ButtonText) 20 | form.SetFieldBackgroundColor(DefaultColorSchema.PasswordMgmtColors.FieldBackground) 21 | form.SetFieldTextColor(DefaultColorSchema.PasswordMgmtColors.FieldText) 22 | svc := tview.NewInputField().SetLabel("Service:").SetFieldWidth(40) 23 | login := tview.NewInputField().SetLabel("Login:").SetFieldWidth(40) 24 | comment := tview.NewInputField().SetLabel("Comment:").SetFieldWidth(40) 25 | setupPwgGen := func(i *tview.InputField) *tview.InputField { 26 | i.SetPlaceholder("Press 'Alt-G' to generate"). 27 | SetInputCapture(func(k *tcell.EventKey) *tcell.EventKey { 28 | if k.Modifiers()&tcell.ModAlt > 0 && k.Rune() == 'g' { 29 | genPwdFunc() 30 | form.SetFocus(4) 31 | e.app.SetFocus(form) 32 | return nil 33 | } else { 34 | return k 35 | } 36 | }) 37 | return i 38 | } 39 | pwd := setupPwgGen(tview.NewInputField().SetLabel("Password:").SetFieldWidth(40). 40 | SetMaskCharacter('*')) 41 | confirm := setupPwgGen(tview.NewInputField().SetLabel("Confirm:").SetFieldWidth(40). 42 | SetMaskCharacter('*')) 43 | genPwdFunc = func() { 44 | password := uniuri.NewLen(rand.Intn(8) + 8) 45 | pwd.SetText(password) 46 | confirm.SetText(password) 47 | } 48 | form. 49 | AddFormItem(svc). 50 | AddFormItem(login). 51 | AddFormItem(pwd). 52 | AddFormItem(confirm). 53 | AddFormItem(comment). 54 | AddButton("Ok", func() { 55 | e.app.QueueUpdateDraw(func() { 56 | switch { 57 | case pwd.GetText() == "": 58 | e.showMessage("Error", "Empty password", screenPwdManage, errorsMessageStyle) 59 | case pwd.GetText() != confirm.GetText(): 60 | e.showMessage("Error", "Passwords mismatch", screenPwdManage, errorsMessageStyle) 61 | default: 62 | p := ezpwd.Password{ 63 | Service: svc.GetText(), 64 | Login: login.GetText(), 65 | Password: pwd.GetText(), 66 | Comment: comment.GetText(), 67 | } 68 | if id == -1 { 69 | pwds = append(pwds, p) 70 | } else { 71 | pwds[id] = p 72 | } 73 | pwdModified = true 74 | e.passwordsChan <- pwds 75 | } 76 | }) 77 | }). 78 | AddButton("Cancel", func() { 79 | e.pages.RemovePage(screenPwdManage) 80 | e.pages.ShowPage(screenPwds) 81 | }) 82 | if id == -1 { 83 | form.SetBorderColor(DefaultColorSchema.PasswordMgmtColors.BorderAdd) 84 | form.SetTitleColor(DefaultColorSchema.PasswordMgmtColors.TitleAdd) 85 | form.SetTitle(" Adding new login ") 86 | } else { 87 | p := pwds[id] 88 | form.SetBorderColor(DefaultColorSchema.PasswordMgmtColors.BorderUpdate) 89 | form.SetTitleColor(DefaultColorSchema.PasswordMgmtColors.TitleUpdate) 90 | form.SetTitle(fmt.Sprintf(" Updating %s : %s ", p.Service, p.Login)) 91 | svc.SetText(p.Service) 92 | login.SetText(p.Login) 93 | pwd.SetText(p.Password) 94 | confirm.SetText(p.Password) 95 | comment.SetText(p.Comment) 96 | } 97 | form.SetTitleAlign(tview.AlignCenter) 98 | form.SetBorder(true) 99 | form.SetCancelFunc(func() { 100 | e.pages.RemovePage(screenPwdManage) 101 | e.pages.ShowPage(screenPwds) 102 | }) 103 | return form 104 | } 105 | -------------------------------------------------------------------------------- /ezpwd_tui/passwords_form.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/gdamore/tcell" 10 | "github.com/jdevelop/ezpwd" 11 | "github.com/rivo/tview" 12 | ) 13 | 14 | func (e *devEzpwd) initForm() *tview.Form { 15 | form := tview.NewForm().SetButtonsAlign(tview.AlignCenter) 16 | initFormColors(form) 17 | pwd := tview.NewInputField(). 18 | SetLabel("Password"). 19 | SetFieldWidth(20). 20 | SetMaskCharacter('*') 21 | confirm := tview.NewInputField(). 22 | SetLabel("Confirm"). 23 | SetFieldWidth(20). 24 | SetMaskCharacter('*') 25 | onComplete := func() { 26 | if pwd.GetText() == "" { 27 | e.showMessage("Error", "Password can't be empty", screenPwd, errorsMessageStyle) 28 | return 29 | } 30 | if pwd.GetText() != confirm.GetText() { 31 | e.showMessage("Error", "Password doesn't match confirmation", screenPwd, errorsMessageStyle) 32 | return 33 | } 34 | crypto, err := ezpwd.NewCrypto([]byte(pwd.GetText())) 35 | if err != nil { 36 | e.showMessage("Error", fmt.Sprintf("can't create crypto: %v", err), screenPwd, errorsMessageStyle) 37 | return 38 | } 39 | e.crypto = crypto 40 | e.passwordsChan <- []ezpwd.Password{} 41 | } 42 | form.SetCancelFunc(e.app.Stop) 43 | form. 44 | AddFormItem(pwd). 45 | AddFormItem(confirm). 46 | AddButton("Create", func() { 47 | onComplete() 48 | }). 49 | AddButton("Quit", func() { 50 | e.app.Stop() 51 | }) 52 | form.SetBorder(true).SetTitle(" Create password storage ").SetTitleAlign(tview.AlignCenter) 53 | form.SetFocus(0) 54 | return form 55 | } 56 | 57 | func initFormColors(form *tview.Form) { 58 | form.SetBackgroundColor(DefaultColorSchema.LoginFormColors.Background) 59 | form.SetTitleColor(DefaultColorSchema.LoginFormColors.Title) 60 | form.SetBorderColor(DefaultColorSchema.LoginFormColors.Border) 61 | form.SetLabelColor(DefaultColorSchema.LoginFormColors.Label) 62 | form.SetButtonBackgroundColor(DefaultColorSchema.LoginFormColors.ButtonBackground) 63 | form.SetButtonTextColor(DefaultColorSchema.LoginFormColors.ButtonText) 64 | form.SetFieldBackgroundColor(DefaultColorSchema.LoginFormColors.FieldBackground) 65 | form.SetFieldTextColor(DefaultColorSchema.LoginFormColors.FieldText) 66 | } 67 | 68 | func (e *devEzpwd) passwordForm() *tview.Form { 69 | form := tview.NewForm().SetButtonsAlign(tview.AlignCenter) 70 | initFormColors(form) 71 | onComplete := func(pwd string) { 72 | crypto, err := ezpwd.NewCrypto([]byte(pwd)) 73 | if err != nil { 74 | e.showMessage("Error", fmt.Sprintf("can't create crypto: %v", err), screenPwd, errorsMessageStyle) 75 | return 76 | } 77 | f, err := os.Open(e.passwordPath) 78 | switch { 79 | case err == nil: 80 | var buf bytes.Buffer 81 | if err := crypto.Decrypt(f, &buf); err != nil { 82 | e.showMessage("Error", fmt.Sprintf("can't descrypt storage: %v", err), screenPwd, errorsMessageStyle) 83 | return 84 | } 85 | if pwds, err := ezpwd.ReadPasswords(&buf); err != nil { 86 | e.showMessage("Error", fmt.Sprintf("can't read passwords: %v", err), screenPwd, errorsMessageStyle) 87 | return 88 | } else { 89 | e.crypto = crypto 90 | e.passwordsChan <- pwds 91 | } 92 | case errors.Is(err, os.ErrNotExist): 93 | e.crypto = crypto 94 | e.passwordsChan <- []ezpwd.Password{} 95 | default: 96 | e.showMessage("Error", fmt.Sprintf("can't open file: %s : %v : %T", e.passwordPath, err, err), screenPwd, errorsMessageStyle) 97 | } 98 | } 99 | pwd := tview.NewInputField(). 100 | SetLabel("Password"). 101 | SetFieldWidth(20). 102 | SetMaskCharacter('*') 103 | 104 | escHandler := func() { 105 | if pwd.GetText() == "" { 106 | e.app.Stop() 107 | } else { 108 | pwd.SetText("") 109 | e.app.SetFocus(pwd) 110 | } 111 | } 112 | pwd. 113 | SetDoneFunc(func(key tcell.Key) { 114 | switch key { 115 | case tcell.KeyEnter: 116 | onComplete(pwd.GetText()) 117 | case tcell.KeyEsc: 118 | escHandler() 119 | } 120 | }) 121 | form.SetCancelFunc(escHandler) 122 | btnOk := tview.NewButton("Unlock").SetSelectedFunc(func() { onComplete(pwd.GetText()) }) 123 | btnOk.SetBackgroundColor(tcell.ColorRed) 124 | form.AddFormItem(pwd). 125 | AddButton("Unlock", func() { onComplete(pwd.GetText()) }). 126 | AddButton("Quit", e.app.Stop) 127 | form.SetBorder(true).SetTitle(" Unlock password storage ").SetTitleAlign(tview.AlignCenter) 128 | form.SetFocus(0) 129 | return form 130 | } 131 | -------------------------------------------------------------------------------- /ezpwd_tui/passwords_table.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/atotto/clipboard" 11 | "github.com/gdamore/tcell" 12 | "github.com/jdevelop/ezpwd" 13 | "github.com/rivo/tview" 14 | ) 15 | 16 | var pwdModified = false 17 | 18 | func (e *devEzpwd) passwordsTable() { 19 | table := tview.NewTable(). 20 | SetBorders(true) 21 | table.SetSelectable(true, false) 22 | table.SetFixed(1, 0) 23 | table.SetBackgroundColor(DefaultColorSchema.PasswordsTableColors.Background) 24 | table.SetTitleColor(DefaultColorSchema.PasswordsTableColors.Title) 25 | table.SetBorderColor(DefaultColorSchema.PasswordsTableColors.Border) 26 | table.SetBordersColor(DefaultColorSchema.PasswordsTableColors.Border) 27 | table.SetSelectedStyle(DefaultColorSchema.PasswordsTableColors.Selection, DefaultColorSchema.PasswordsTableColors.SelectionBackground, 0) 28 | filterBox := tview.NewInputField().SetLabel("Filter: ") 29 | filterBox.SetBackgroundColor(DefaultColorSchema.PasswordsTableColors.Background) 30 | filterBox.SetFieldTextColor(DefaultColorSchema.PasswordsTableColors.FieldText) 31 | filterBox.SetFieldBackgroundColor(DefaultColorSchema.PasswordsTableColors.FieldBackground) 32 | filterBox.SetLabelColor(DefaultColorSchema.PasswordsTableColors.Label) 33 | passwordsMsg := tview.NewTextView() 34 | passwordsMsg.SetBorder(true).SetTitleAlign(tview.AlignCenter) 35 | passwordsMsg.SetBackgroundColor(DefaultColorSchema.PasswordsTableColors.CopiedBackground) 36 | passwordsMsg.SetTextColor(DefaultColorSchema.PasswordsTableColors.CopiedText) 37 | passwordsMsg.SetTitleColor(DefaultColorSchema.PasswordsTableColors.CopiedTitle) 38 | passwordsMsg.SetBorderColor(DefaultColorSchema.PasswordsTableColors.CopiedBorder) 39 | passwordsMsg.SetDoneFunc(func(tcell.Key) { 40 | e.pages.RemovePage(screenPwdCopied) 41 | e.pages.ShowPage(screenPwds) 42 | }) 43 | 44 | go func(t *tview.Table) { 45 | var ( 46 | currentPasswords *[]ezpwd.Password 47 | drawTable func(string) 48 | mappings []int 49 | ) 50 | 51 | t.SetDoneFunc(func(k tcell.Key) { 52 | switch k { 53 | case tcell.KeyEsc: 54 | if filterBox.GetText() != "" { 55 | e.app.QueueUpdateDraw(func() { 56 | filterBox.SetText("") 57 | drawTable("") 58 | e.app.SetFocus(table) 59 | e.app.Draw() 60 | }) 61 | } else { 62 | if pwdModified { 63 | e.confirm(" Unsaved changes! Are you sure you want to quit? ", screenPwds, e.app.Stop) 64 | } else { 65 | e.confirm(" Are you sure you want to quit? ", screenPwds, e.app.Stop) 66 | } 67 | } 68 | } 69 | }) 70 | 71 | filterBox.SetDoneFunc(func(key tcell.Key) { 72 | switch key { 73 | case tcell.KeyEnter: 74 | e.app.QueueUpdateDraw(func() { 75 | drawTable(filterBox.GetText()) 76 | e.app.SetFocus(table) 77 | e.app.Draw() 78 | }) 79 | case tcell.KeyEsc: 80 | filterBox.SetText("") 81 | e.app.QueueUpdateDraw(func() { 82 | drawTable("") 83 | e.app.SetFocus(table) 84 | e.app.Draw() 85 | }) 86 | } 87 | }) 88 | drawTable = func(filter string) { 89 | t.Clear() 90 | mappings = make([]int, 0) 91 | table.SetSelectedFunc(func(row, col int) { 92 | if row == 0 { 93 | return 94 | } 95 | row = mappings[row-1] 96 | clipboard.WriteAll((*currentPasswords)[row].Password) 97 | var content = fmt.Sprintf(" Password copied to clipboard '%s : %s' ", (*currentPasswords)[row].Service, (*currentPasswords)[row].Login) 98 | passwordsMsg.SetText(content) 99 | mp := modal(passwordsMsg, len(content)+2, 3) 100 | e.pages.AddPage(screenPwdCopied, mp, true, true) 101 | e.pages.ShowPage(screenPwdCopied) 102 | e.app.SetFocus(passwordsMsg) 103 | }) 104 | type ColSpec struct { 105 | name string 106 | expansion int 107 | } 108 | for i, v := range []ColSpec{{"#", 1}, {"Service", 4}, {"Username", 5}, {"Comment", 10}} { 109 | table.SetCell(0, i, tview.NewTableCell(v.name). 110 | SetAlign(tview.AlignCenter). 111 | SetTextColor(DefaultColorSchema.PasswordsTableColors.Header). 112 | SetExpansion(v.expansion), 113 | ) 114 | } 115 | i := 1 116 | equals := func(src, substr string) bool { 117 | return strings.Contains(strings.ToUpper(src), strings.ToUpper(substr)) 118 | } 119 | for idx, p := range *currentPasswords { 120 | if filter != "" && !(equals(p.Service, filter) || equals(p.Comment, filter) || equals(p.Login, filter)) { 121 | continue 122 | } 123 | mappings = append(mappings, idx) 124 | table.SetCell(i, 0, tview.NewTableCell(fmt.Sprintf("%d", i)).SetAlign(tview.AlignCenter)) 125 | table.SetCellSimple(i, 1, p.Service) 126 | table.SetCellSimple(i, 2, p.Login) 127 | table.SetCellSimple(i, 3, p.Comment) 128 | i += 1 129 | } 130 | table.ScrollToBeginning() 131 | 132 | } 133 | dialogsStyle := func(b *tview.Box) { 134 | b.SetBackgroundColor(DefaultColorSchema.PasswordsTableColors.Background) 135 | } 136 | table.SetInputCapture(func(key *tcell.EventKey) *tcell.EventKey { 137 | switch key.Rune() { 138 | case 'a', 'A': 139 | e.app.QueueUpdateDraw(func() { 140 | e.pages.AddPage(screenPwdManage, 141 | modal(e.passwordMgmtForm(-1, *currentPasswords), 50, 15, dialogsStyle), 142 | true, true, 143 | ) 144 | e.pages.ShowPage(screenPwdManage) 145 | }) 146 | case 'u', 'U': 147 | r, _ := table.GetSelection() 148 | if r == 0 || len(*currentPasswords) == 0 { 149 | break 150 | } 151 | r = mappings[r-1] 152 | e.app.QueueUpdateDraw(func() { 153 | e.pages.AddPage(screenPwdManage, 154 | modal(e.passwordMgmtForm(r, *currentPasswords), 50, 15, dialogsStyle), 155 | true, true, 156 | ) 157 | e.pages.ShowPage(screenPwdManage) 158 | }) 159 | case 'd', 'D': 160 | r, _ := table.GetSelection() 161 | if r == 0 || r-1 >= len(*currentPasswords) { 162 | break 163 | } 164 | r = mappings[r-1] 165 | e.confirm(fmt.Sprintf(" Remove service '%s : %s'? ", (*currentPasswords)[r].Service, (*currentPasswords)[r].Login), screenPwds, func() { 166 | e.app.QueueUpdateDraw(func() { 167 | *currentPasswords = append((*currentPasswords)[:r], (*currentPasswords)[r+1:]...) 168 | pwdModified = true 169 | drawTable(filterBox.GetText()) 170 | }) 171 | }) 172 | case 'f', 'F': 173 | e.app.SetFocus(filterBox) 174 | case 's', 'S': 175 | { 176 | err := backup(e.passwordPath) 177 | switch { 178 | case err == nil || errors.Is(err, os.ErrNotExist): 179 | // do nothing 180 | default: 181 | e.showMessage("Error", fmt.Sprintf("Can't backup password file: %+v", err), screenPwds, errorsMessageStyle) 182 | break 183 | } 184 | } 185 | var buffer bytes.Buffer 186 | if err := ezpwd.WritePasswords(*currentPasswords, &buffer); err != nil { 187 | e.showMessage("Error", fmt.Sprintf("Can't backup password file: %+v", err), screenPwds, errorsMessageStyle) 188 | break 189 | } 190 | _file, err := os.Create(e.passwordPath) 191 | if err != nil { 192 | e.showMessage("Error", fmt.Sprintf("Can't open password file '%s': %+v", e.passwordPath, err), screenPwds, errorsMessageStyle) 193 | break 194 | } 195 | defer _file.Close() 196 | if err := e.crypto.Encrypt(&buffer, _file); err != nil { 197 | e.showMessage("Error", fmt.Sprintf("Can't encrypt password file '%s': %+v", e.passwordPath, err), screenPwds, errorsMessageStyle) 198 | } else { 199 | e.showMessage("Success!", fmt.Sprintf("Passwords saved successfully"), screenPwds, successMessageStyle) 200 | } 201 | pwdModified = false 202 | } 203 | return key 204 | }) 205 | 206 | for pwds := range e.passwordsChan { 207 | currentPasswords = &pwds 208 | drawTable("") 209 | e.app.QueueUpdateDraw(func() { 210 | e.pages.SwitchToPage(screenPwds) 211 | e.app.SetFocus(table) 212 | e.app.Draw() 213 | }) 214 | } 215 | }(table) 216 | makeButton := func(txt string) *tview.TextView { 217 | btn := tview.NewTextView() 218 | btn.SetBackgroundColor(DefaultColorSchema.PasswordsTableColors.ButtonBackground) 219 | btn.SetTextColor(DefaultColorSchema.PasswordsTableColors.ButtonText) 220 | return btn.SetText(fmt.Sprintf("[%s]%c[%s]%s", color2Hex(DefaultColorSchema.PasswordsTableColors.ButtonAccent), txt[0], 221 | color2Hex(DefaultColorSchema.PasswordsTableColors.ButtonText), txt[1:])). 222 | SetTextAlign(tview.AlignCenter).SetDynamicColors(true) 223 | } 224 | flexColors := func(flex *tview.Flex) *tview.Flex { 225 | flex.SetBackgroundColor(DefaultColorSchema.PasswordsTableColors.Background) 226 | flex.SetTitleColor(DefaultColorSchema.PasswordsTableColors.Title) 227 | flex.SetBorderColor(DefaultColorSchema.PasswordsTableColors.Background) 228 | return flex 229 | } 230 | spacerBox := func() *tview.Box { 231 | return tview.NewBox().SetBackgroundColor(DefaultColorSchema.PasswordMgmtColors.Background) 232 | } 233 | e.pages.AddPage(screenPwds, flexColors(tview.NewFlex(). 234 | SetDirection(tview.FlexRow). 235 | AddItem(filterBox, 2, 0, false). 236 | AddItem( 237 | flexColors(tview.NewFlex(). 238 | AddItem(spacerBox(), 0, 1, false). 239 | AddItem(table, 0, 20, true). 240 | AddItem(spacerBox(), 0, 1, false)), 0, 1, true, 241 | )). 242 | AddItem( 243 | flexColors(tview.NewFlex(). 244 | AddItem(spacerBox(), 0, 2, false). 245 | AddItem(makeButton("Filter"), 0, 4, true). 246 | AddItem(spacerBox(), 0, 2, false). 247 | AddItem(makeButton("Add"), 0, 4, true). 248 | AddItem(spacerBox(), 0, 2, false). 249 | AddItem(makeButton("Update"), 0, 4, true). 250 | AddItem(spacerBox(), 0, 1, false). 251 | AddItem(makeButton("Delete"), 0, 4, true). 252 | AddItem(spacerBox(), 0, 2, false). 253 | AddItem(makeButton("Save"), 0, 4, true). 254 | AddItem(spacerBox(), 0, 2, false)), 255 | 1, 1, true, 256 | ).SetFullScreen(false), true, false) 257 | } 258 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jdevelop/ezpwd 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/atotto/clipboard v0.1.2 7 | github.com/davecgh/go-spew v1.1.1 // indirect 8 | github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 9 | github.com/gdamore/tcell v1.3.0 10 | github.com/mailru/easyjson v0.7.0 11 | github.com/olekukonko/tablewriter v0.0.4 12 | github.com/rivo/tview v0.0.0-20191129065140-82b05c9fb329 13 | github.com/stretchr/testify v1.4.0 14 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd 15 | ) 16 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package ezpwd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/olekukonko/tablewriter" 8 | ) 9 | 10 | type Renderer interface { 11 | RenderPasswords([]Password) 12 | } 13 | 14 | type hidePasswords struct { 15 | w io.Writer 16 | } 17 | 18 | type showPasswords struct { 19 | w io.Writer 20 | } 21 | 22 | func NewHiddenPWDs(w io.Writer) *hidePasswords { 23 | return &hidePasswords{w: w} 24 | } 25 | 26 | func NewShownPWDs(w io.Writer) *showPasswords { 27 | return &showPasswords{w: w} 28 | } 29 | 30 | func (hp *hidePasswords) RenderPasswords(pwds []Password) { 31 | table := tablewriter.NewWriter(hp.w) 32 | for i, pwd := range pwds { 33 | table.Append([]string{fmt.Sprintf("%d", i), pwd.Service, pwd.Login, pwd.Comment}) 34 | } 35 | table.SetHeader([]string{"#", "Service", "Login", "Comment"}) 36 | table.Render() 37 | } 38 | 39 | func (sp *showPasswords) RenderPasswords(pwds []Password) { 40 | table := tablewriter.NewWriter(sp.w) 41 | for i, pwd := range pwds { 42 | table.Append([]string{fmt.Sprintf("%d", i), pwd.Service, pwd.Login, pwd.Password, pwd.Comment}) 43 | } 44 | table.SetHeader([]string{"#", "Service", "Login", "Password", "Comment"}) 45 | table.Render() 46 | } 47 | 48 | var _ Renderer = &showPasswords{} 49 | var _ Renderer = &hidePasswords{} 50 | -------------------------------------------------------------------------------- /render_test.go: -------------------------------------------------------------------------------- 1 | package ezpwd 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var pwds = []Password{ 11 | { 12 | Service: "Gmail", 13 | Login: "123@123.com", 14 | Password: "password", 15 | }, 16 | { 17 | Service: "Yahoo", 18 | Login: "123@yahoo.com", 19 | Password: "nopassword", 20 | }, 21 | } 22 | 23 | func TestRenderNoPassword(t *testing.T) { 24 | expected := `+---+---------+---------------+---------+ 25 | | # | SERVICE | LOGIN | COMMENT | 26 | +---+---------+---------------+---------+ 27 | | 0 | Gmail | 123@123.com | | 28 | | 1 | Yahoo | 123@yahoo.com | | 29 | +---+---------+---------------+---------+ 30 | ` 31 | w := bytes.Buffer{} 32 | pwdWriter := NewHiddenPWDs(&w) 33 | pwdWriter.RenderPasswords(pwds) 34 | content := string(w.Bytes()) 35 | assert.Equal(t, expected, content) 36 | } 37 | 38 | func TestRenderAllPassword(t *testing.T) { 39 | expected := `+---+---------+---------------+------------+---------+ 40 | | # | SERVICE | LOGIN | PASSWORD | COMMENT | 41 | +---+---------+---------------+------------+---------+ 42 | | 0 | Gmail | 123@123.com | password | | 43 | | 1 | Yahoo | 123@yahoo.com | nopassword | | 44 | +---+---------+---------------+------------+---------+ 45 | ` 46 | w := bytes.Buffer{} 47 | pwdWriter := NewShownPWDs(&w) 48 | pwdWriter.RenderPasswords(pwds) 49 | content := string(w.Bytes()) 50 | assert.Equal(t, expected, content) 51 | } 52 | -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | package ezpwd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | var splitter = regexp.MustCompile("\\s/\\s") 12 | 13 | type Password struct { 14 | Service string 15 | Login string 16 | Password string 17 | Comment string 18 | } 19 | 20 | func ReadPasswords(src io.Reader) ([]Password, error) { 21 | passwords := make([]Password, 0, 50) 22 | rdr := bufio.NewScanner(src) 23 | for rdr.Scan() { 24 | line := rdr.Text() 25 | parts := splitter.Split(line, 4) 26 | pLen := len(parts) 27 | if pLen < 3 || pLen > 4 { 28 | continue 29 | } 30 | p := Password{ 31 | Service: parts[0], 32 | Login: parts[1], 33 | Password: parts[2], 34 | } 35 | if pLen == 4 { 36 | p.Comment = parts[3] 37 | } 38 | passwords = append(passwords, p) 39 | } 40 | return passwords, nil 41 | } 42 | 43 | func WritePasswords(passwords []Password, writer io.Writer) error { 44 | wrtr := bufio.NewWriter(writer) 45 | for _, pwd := range passwords { 46 | if pwd.Comment != "" { 47 | if _, err := wrtr.WriteString(fmt.Sprintf("%s / %s / %s / %s\n", strings.TrimSpace(pwd.Service), 48 | strings.TrimSpace(pwd.Login), strings.TrimSpace(pwd.Password), strings.TrimSpace(pwd.Comment)), 49 | ); err != nil { 50 | return err 51 | } 52 | } else { 53 | if _, err := wrtr.WriteString(fmt.Sprintf("%s / %s / %s\n", 54 | strings.TrimSpace(pwd.Service), 55 | strings.TrimSpace(pwd.Login), 56 | strings.TrimSpace(pwd.Password)), 57 | ); err != nil { 58 | return err 59 | } 60 | } 61 | } 62 | return wrtr.Flush() 63 | } 64 | -------------------------------------------------------------------------------- /storage_test.go: -------------------------------------------------------------------------------- 1 | package ezpwd 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var expected = []Password{ 12 | { 13 | Service: "service", 14 | Login: "username", 15 | Password: "password", 16 | Comment: "", 17 | }, 18 | { 19 | Service: "another service", 20 | Login: "user", 21 | Password: "pwd", 22 | Comment: "comment", 23 | }, 24 | } 25 | 26 | func TestPasswordRead(t *testing.T) { 27 | rdr := strings.NewReader(`service / username / password 28 | another service / user / pwd / comment 29 | no service 30 | #comment`) 31 | pwds, err := ReadPasswords(rdr) 32 | assert.Nil(t, err) 33 | assert.EqualValues(t, expected[:], pwds) 34 | } 35 | 36 | func TestPasswordWrite(t *testing.T) { 37 | b := new(bytes.Buffer) 38 | err := WritePasswords(expected, b) 39 | 40 | assert.Nil(t, err) 41 | 42 | assert.Equal(t, `service / username / password 43 | another service / user / pwd / comment 44 | `, b.String()) 45 | } 46 | --------------------------------------------------------------------------------