From 9f897707d8c6a24dc6279a8f7e0316a3b6f08a7d Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Sat, 24 Mar 2018 21:26:05 +0100
Subject: [PATCH 01/35] Allow using multiple password stores

Known limitation: two password stores cannot have identical file names
---
 browserpass.go          |  16 +++---
 cmd/browserpass/main.go |   8 +--
 pass/disk.go            | 116 ++++++++++++++++++++++------------------
 pass/disk_test.go       |  63 ++++++++++++----------
 pass/pass.go            |   6 ---
 5 files changed, 107 insertions(+), 102 deletions(-)

diff --git a/browserpass.go b/browserpass.go
index 93a155e..b9b5f64 100644
--- a/browserpass.go
+++ b/browserpass.go
@@ -35,7 +35,8 @@ var endianness = binary.LittleEndian
 // which is then passed along with the command over the native messaging api.
 type Config struct {
 	// Manual searches use FuzzySearch if true, GlobSearch otherwise
-	UseFuzzy bool `json:"use_fuzzy_search"`
+	UseFuzzy    bool     `json:"use_fuzzy_search"`
+	Directories []string `json:"directories"`
 }
 
 // msg defines a message sent from a browser extension.
@@ -47,7 +48,7 @@ type msg struct {
 }
 
 // Run starts browserpass.
-func Run(stdin io.Reader, stdout io.Writer, s pass.Store) error {
+func Run(stdin io.Reader, stdout io.Writer) error {
 	protector.Protect("stdio rpath proc exec")
 	for {
 		// Get message length, 4 bytes
@@ -65,9 +66,10 @@ func Run(stdin io.Reader, stdout io.Writer, s pass.Store) error {
 			return err
 		}
 
-		// Since the pass.Store object is created by the wrapper prior to
-		// settings from the browser being made available, we set them here
-		s.SetConfig(&data.Settings.UseFuzzy)
+		s, err := pass.NewDefaultStore(data.Settings.Directories, data.Settings.UseFuzzy)
+		if err != nil {
+			return err
+		}
 
 		var resp interface{}
 		switch data.Action {
@@ -185,7 +187,7 @@ func parseTotp(str string, l *Login) error {
 
 	if ourl == "" {
 		tokenPattern := regexp.MustCompile("(?i)^totp(-secret)?:")
-		token := tokenPattern.ReplaceAllString(str, "");
+		token := tokenPattern.ReplaceAllString(str, "")
 		if len(token) != len(str) {
 			ourl = "otpauth://totp/?secret=" + strings.TrimSpace(token)
 		}
@@ -222,7 +224,7 @@ func parseLogin(r io.Reader) (*Login, error) {
 		if len(replaced) != len(line) {
 			login.Username = strings.TrimSpace(replaced)
 		}
-		if (login.URL == "") {
+		if login.URL == "" {
 			replaced = urlPattern.ReplaceAllString(line, "")
 			if len(replaced) != len(line) {
 				login.URL = strings.TrimSpace(replaced)
diff --git a/cmd/browserpass/main.go b/cmd/browserpass/main.go
index aae811a..0345e5e 100644
--- a/cmd/browserpass/main.go
+++ b/cmd/browserpass/main.go
@@ -6,7 +6,6 @@ import (
 	"os"
 
 	"github.com/dannyvankooten/browserpass"
-	"github.com/dannyvankooten/browserpass/pass"
 	"github.com/dannyvankooten/browserpass/protector"
 )
 
@@ -14,12 +13,7 @@ func main() {
 	protector.Protect("stdio rpath proc exec getpw")
 	log.SetPrefix("[Browserpass] ")
 
-	s, err := pass.NewDefaultStore()
-	if err != nil {
-		log.Fatal(err)
-	}
-
-	if err := browserpass.Run(os.Stdin, os.Stdout, s); err != nil && err != io.EOF {
+	if err := browserpass.Run(os.Stdin, os.Stdout); err != nil && err != io.EOF {
 		log.Fatal(err)
 	}
 }
diff --git a/pass/disk.go b/pass/disk.go
index cbd7f94..39a9761 100644
--- a/pass/disk.go
+++ b/pass/disk.go
@@ -15,52 +15,54 @@ import (
 )
 
 type diskStore struct {
-	path     string
+	paths    []string
 	useFuzzy bool // Setting for FuzzySearch or GlobSearch in manual searches
 }
 
-func NewDefaultStore() (Store, error) {
-	path, err := defaultStorePath()
-	if err != nil {
-		return nil, err
+func NewDefaultStore(paths []string, useFuzzy bool) (Store, error) {
+	if paths == nil || len(paths) == 0 {
+		defaultPaths, err := defaultStorePath()
+		if err != nil {
+			return nil, err
+		}
+		paths = defaultPaths
 	}
 
-	return &diskStore{path, false}, nil
-}
-
-func defaultStorePath() (string, error) {
-	usr, err := user.Current()
-
-	if err != nil {
-		return "", err
+	// Follow symlinks
+	finalPaths := make([]string, len(paths))
+	for i, path := range paths {
+		finalPath, err := filepath.EvalSymlinks(path)
+		if err != nil {
+			return nil, err
+		}
+		finalPaths[i] = finalPath
 	}
 
+	return &diskStore{finalPaths, useFuzzy}, nil
+}
+
+func defaultStorePath() ([]string, error) {
 	path := os.Getenv("PASSWORD_STORE_DIR")
-	if path == "" {
-		path = filepath.Join(usr.HomeDir, ".password-store")
+	if path != "" {
+		return []string{path}, nil
 	}
 
-	// Follow symlinks
-	return filepath.EvalSymlinks(path)
-}
-
-// Set the configuration options for password matching.
-func (s *diskStore) SetConfig(use_fuzzy *bool) error {
-	if use_fuzzy != nil {
-		s.useFuzzy = *use_fuzzy
+	usr, err := user.Current()
+	if err != nil {
+		return nil, err
 	}
-	return nil
+
+	path = filepath.Join(usr.HomeDir, ".password-store")
+	return []string{path}, nil
 }
 
 // Do a search. Will call into the correct algoritm (glob or fuzzy)
 // based on the settings present in the diskStore struct
 func (s *diskStore) Search(query string) ([]string, error) {
-	// default glob search
-	if !s.useFuzzy {
-		return s.GlobSearch(query)
-	} else {
+	if s.useFuzzy {
 		return s.FuzzySearch(query)
 	}
+	return s.GlobSearch(query)
 }
 
 // Fuzzy searches first get a list of all pass entries by doing a glob search
@@ -68,7 +70,7 @@ func (s *diskStore) Search(query string) ([]string, error) {
 // a slice of strings, finally returning all of the unique entries.
 func (s *diskStore) FuzzySearch(query string) ([]string, error) {
 	var items []string
-	file_list, err := s.GlobSearch("")
+	fileList, err := s.GlobSearch("")
 	if err != nil {
 		return nil, err
 	}
@@ -76,9 +78,9 @@ func (s *diskStore) FuzzySearch(query string) ([]string, error) {
 	// The resulting match struct does not copy the strings, but rather
 	// provides the index to the original array. Copy those strings
 	// into the result slice
-	matches := sfuzzy.Find(query, file_list)
+	matches := sfuzzy.Find(query, fileList)
 	for _, match := range matches {
-		items = append(items, file_list[match.Index])
+		items = append(items, fileList[match.Index])
 	}
 
 	return items, nil
@@ -90,24 +92,33 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) {
 	//	2. DOMAIN.gpg
 	//	3. DOMAIN/SUBDIRECTORY/USERNAME.gpg
 
-	matches, err := zglob.GlobFollowSymlinks(s.path + "/**/" + query + "*/**/*.gpg")
-	if err != nil {
-		return nil, err
-	}
+	items := []string{}
 
-	matches2, err := zglob.GlobFollowSymlinks(s.path + "/**/" + query + "*.gpg")
-	if err != nil {
-		return nil, err
-	}
+	for _, path := range s.paths {
+		matches, err := zglob.GlobFollowSymlinks(path + "/**/" + query + "*/**/*.gpg")
+		if err != nil {
+			return nil, err
+		}
 
-	items := append(matches, matches2...)
-	for i, path := range items {
-		item, err := filepath.Rel(s.path, path)
+		matches2, err := zglob.GlobFollowSymlinks(path + "/**/" + query + "*.gpg")
 		if err != nil {
 			return nil, err
 		}
-		items[i] = strings.TrimSuffix(item, ".gpg")
+
+		allMatches := append(matches, matches2...)
+
+		for i, match := range allMatches {
+			// TODO this does not handle identical file names in multiple s.paths
+			item, err := filepath.Rel(path, match)
+			if err != nil {
+				return nil, err
+			}
+			allMatches[i] = strings.TrimSuffix(item, ".gpg")
+		}
+
+		items = append(items, allMatches...)
 	}
+
 	if strings.Count(query, ".") >= 2 {
 		// try finding additional items by removing subparts of the query
 		queryParts := strings.SplitN(query, ".", 2)[1:]
@@ -125,17 +136,16 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) {
 }
 
 func (s *diskStore) Open(item string) (io.ReadCloser, error) {
-	p := filepath.Join(s.path, item+".gpg")
-	if !filepath.HasPrefix(p, s.path) {
-		// Make sure the requested item is *in* the password store
-		return nil, errors.New("invalid item path")
-	}
-
-	f, err := os.Open(p)
-	if os.IsNotExist(err) {
-		return nil, ErrNotFound
+	for _, path := range s.paths {
+		path := filepath.Join(path, item+".gpg")
+		f, err := os.Open(path)
+		if os.IsNotExist(err) {
+			continue
+		}
+		// TODO this does not handle identical file names in multiple s.paths
+		return f, err
 	}
-	return f, err
+	return nil, errors.New("Unable to find the item on disk")
 }
 
 func unique(elems []string) []string {
diff --git a/pass/disk_test.go b/pass/disk_test.go
index 5892e63..7ea51d8 100644
--- a/pass/disk_test.go
+++ b/pass/disk_test.go
@@ -1,7 +1,6 @@
 package pass
 
 import (
-	"fmt"
 	"os"
 	"os/user"
 	"path/filepath"
@@ -9,7 +8,8 @@ import (
 )
 
 func TestDefaultStorePath(t *testing.T) {
-	var home, expected, actual string
+	var home, expectedCustom string
+	var expected, actual []string
 
 	usr, err := user.Current()
 
@@ -21,42 +21,47 @@ func TestDefaultStorePath(t *testing.T) {
 
 	// default directory
 	os.Setenv("PASSWORD_STORE_DIR", "")
-	expected = filepath.Join(home, ".password-store")
+	expected = []string{filepath.Join(home, ".password-store")}
 	actual, _ = defaultStorePath()
-	if expected != actual {
-		t.Errorf("1: '%s' does not match '%s'", expected, actual)
+
+	if len(expected) != len(actual) {
+		t.Errorf("1: '%d' does not match '%d'", len(expected), len(actual))
+	}
+
+	if expected[0] != actual[0] {
+		t.Errorf("2: '%s' does not match '%s'", expected[0], actual[0])
 	}
 
 	// custom directory from $PASSWORD_STORE_DIR
-	expected, err = filepath.Abs("browserpass-test")
+	expectedCustom, err = filepath.Abs("browserpass-test")
 	if err != nil {
 		t.Error(err)
 	}
+	expected = []string{expectedCustom}
 
-	fmt.Println(expected)
-	os.Mkdir(expected, os.ModePerm)
-	os.Setenv("PASSWORD_STORE_DIR", expected)
+	os.Mkdir(expectedCustom, os.ModePerm)
+	os.Setenv("PASSWORD_STORE_DIR", expectedCustom)
 	actual, err = defaultStorePath()
 	if err != nil {
 		t.Error(err)
 	}
-	if expected != actual {
-		t.Errorf("2: '%s' does not match '%s'", expected, actual)
+	if len(expected) != len(actual) {
+		t.Errorf("3: '%d' does not match '%d'", len(expected), len(actual))
+	}
+	if expected[0] != actual[0] {
+		t.Errorf("4: '%s' does not match '%s'", expected[0], actual[0])
 	}
 
 	// clean-up
 	os.Setenv("PASSWORD_STORE_DIR", "")
-	os.Remove(expected)
+	os.Remove(expected[0])
 }
 
 func TestDiskStore_Search_nomatch(t *testing.T) {
-	s, err := NewDefaultStore()
-	if err != nil {
-		t.Fatal(err)
-	}
+	store := diskStore{[]string{"test_store"}, false}
 
 	domain := "this-most-definitely-does-not-exist"
-	logins, err := s.Search(domain)
+	logins, err := store.Search(domain)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -66,7 +71,7 @@ func TestDiskStore_Search_nomatch(t *testing.T) {
 }
 
 func TestDiskStoreSearch(t *testing.T) {
-	store := diskStore{"test_store", false}
+	store := diskStore{[]string{"test_store"}, false}
 	targetDomain := "abc.com"
 	testDomains := []string{"abc.com", "test.abc.com", "testing.test.abc.com"}
 	for _, domain := range testDomains {
@@ -89,7 +94,7 @@ func TestDiskStoreSearch(t *testing.T) {
 }
 
 func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testing.T) {
-	store := diskStore{"test_store", false}
+	store := diskStore{[]string{"test_store"}, false}
 	searchResult, err := store.Search("xyz")
 	if err != nil {
 		t.Fatal(err)
@@ -104,7 +109,7 @@ func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testi
 }
 
 func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) {
-	store := diskStore{"test_store", false}
+	store := diskStore{[]string{"test_store"}, false}
 	searchResult, err := store.Search("def.com")
 	if err != nil {
 		t.Fatal(err)
@@ -119,7 +124,7 @@ func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) {
 }
 
 func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) {
-	store := diskStore{"test_store", false}
+	store := diskStore{[]string{"test_store"}, false}
 	searchResult, err := store.Search("amazon.co.uk")
 	if err != nil {
 		t.Fatal(err)
@@ -134,7 +139,7 @@ func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) {
 }
 
 func TestDiskStoreSearchSubDirectories(t *testing.T) {
-	store := diskStore{"test_store", false}
+	store := diskStore{[]string{"test_store"}, false}
 	searchTermsMatches := map[string][]string{
 		"abc.org": []string{"abc.org/user3", "abc.org/wiki/user4", "abc.org/wiki/work/user5"},
 		"wiki":    []string{"abc.org/wiki/user4", "abc.org/wiki/work/user5"},
@@ -158,7 +163,7 @@ func TestDiskStoreSearchSubDirectories(t *testing.T) {
 }
 
 func TestDiskStorePartSearch(t *testing.T) {
-	store := diskStore{"test_store", false}
+	store := diskStore{[]string{"test_store"}, false}
 	searchResult, err := store.Search("ab")
 	if err != nil {
 		t.Fatal(err)
@@ -175,14 +180,14 @@ func TestDiskStorePartSearch(t *testing.T) {
 }
 
 func TestFuzzySearch(t *testing.T) {
-	store := diskStore{"test_store", true}
+	store := diskStore{[]string{"test_store"}, true}
 	searchResult, err := store.Search("amaz2")
 
 	if err != nil {
 		t.Fatal(err)
 	}
 	if len(searchResult) != 2 {
-		t.Fatalf("Result size was: %s expected 2", len(searchResult))
+		t.Fatalf("Result size was: %d expected 2", len(searchResult))
 	}
 
 	expectedResult := map[string]bool{
@@ -198,26 +203,26 @@ func TestFuzzySearch(t *testing.T) {
 }
 
 func TestFuzzySearchNoResult(t *testing.T) {
-	store := diskStore{"test_store", true}
+	store := diskStore{[]string{"test_store"}, true}
 	searchResult, err := store.Search("vvv")
 
 	if err != nil {
 		t.Fatal(err)
 	}
 	if len(searchResult) != 0 {
-		t.Fatalf("Result size was: %s expected 0", len(searchResult))
+		t.Fatalf("Result size was: %d expected 0", len(searchResult))
 	}
 }
 
 func TestFuzzySearchTopLevelEntries(t *testing.T) {
-	store := diskStore{"test_store", true}
+	store := diskStore{[]string{"test_store"}, true}
 	searchResult, err := store.Search("def")
 
 	if err != nil {
 		t.Fatal(err)
 	}
 	if len(searchResult) != 1 {
-		t.Fatalf("Result size was: %s expected 1", len(searchResult))
+		t.Fatalf("Result size was: %d expected 1", len(searchResult))
 	}
 
 	expectedResult := map[string]bool{
diff --git a/pass/pass.go b/pass/pass.go
index d4d5601..b0692d7 100644
--- a/pass/pass.go
+++ b/pass/pass.go
@@ -2,18 +2,12 @@
 package pass
 
 import (
-	"errors"
 	"io"
 )
 
-// ErrNotFound is returned by Store.Open if the requested item is not found.
-var ErrNotFound = errors.New("pass: not found")
-
 // Store is a password store.
 type Store interface {
 	Search(query string) ([]string, error)
 	Open(item string) (io.ReadCloser, error)
 	GlobSearch(query string) ([]string, error)
-	// Update password store settings on the fly
-	SetConfig(use_fuzzy *bool) error
 }

From ce8e255f114474b6cac8caf6938da99447b03583 Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Sat, 24 Mar 2018 22:09:04 +0100
Subject: [PATCH 02/35] Add selector for custom password stores to options

---
 chrome/options.html | 31 +++++++++++++++++++++++++++++--
 chrome/options.js   | 29 +++++++++++++++++++++++++----
 2 files changed, 54 insertions(+), 6 deletions(-)

diff --git a/chrome/options.html b/chrome/options.html
index a471f5d..824714d 100644
--- a/chrome/options.html
+++ b/chrome/options.html
@@ -9,16 +9,43 @@
 <body>
 
 <label>
-  <input type="checkbox" id="auto-submit">
+  <input type="checkbox" id="auto-submit" />
   Auto-Submit login forms
 </label>
 <br/>
+<br/>
+
 <label>
-  <input type="checkbox" id="use-fuzzy">
+  <input type="checkbox" id="use-fuzzy" />
   Use fuzzy search
 </label>
 <br/>
 <br/>
+
+Custom password stores directories: <br/>
+<div class="password-store-group">
+  <input type="checkbox" class="password-store-path-enabled" />
+  <input type="text" class="password-store-path" placeholder="~/.password-store" />
+</div>
+<div class="password-store-group">
+  <input type="checkbox" class="password-store-path-enabled" />
+  <input type="text" class="password-store-path" />
+</div>
+<div class="password-store-group">
+  <input type="checkbox" class="password-store-path-enabled" />
+  <input type="text" class="password-store-path" />
+</div>
+<div class="password-store-group">
+  <input type="checkbox" class="password-store-path-enabled" />
+  <input type="text" class="password-store-path" />
+</div>
+<div class="password-store-group">
+  <input type="checkbox" class="password-store-path-enabled" />
+  <input type="text" class="password-store-path" />
+</div>
+<br/>
+<br/>
+
 <button id="save">Save</button>
 
 <script src="options.js"></script>
diff --git a/chrome/options.js b/chrome/options.js
index ed027ac..d97db69 100644
--- a/chrome/options.js
+++ b/chrome/options.js
@@ -1,23 +1,44 @@
 function save_options() {
-  var autoSubmit = document.getElementById("auto-submit").checked;
+  let autoSubmit = document.getElementById("auto-submit").checked;
   localStorage.setItem("autoSubmit", autoSubmit);
 
   // Options related to fuzzy finding.
   //  use_fuzzy_search indicates if fuzzy finding or glob searching should
   //  be used in manual searches
-  var use_fuzzy = document.getElementById("use-fuzzy").checked;
+  let use_fuzzy = document.getElementById("use-fuzzy").checked;
   localStorage.setItem("use_fuzzy_search", use_fuzzy);
 
+  let groups = document.getElementsByClassName("password-store-group");
+  let paths = [];
+  for (let i = 0; i < groups.length; i++) {
+    let group = groups[i];
+    let enabled = group.querySelector(".password-store-path-enabled").checked;
+    let path = group.querySelector(".password-store-path").value;
+    if (path) {
+      paths.push({ path: path, enabled: enabled });
+    }
+  }
+  localStorage.setItem("paths", JSON.stringify(paths));
+
   window.close();
 }
 
 function restore_options() {
-  var autoSubmit = localStorage.getItem("autoSubmit") == "true";
+  let autoSubmit = localStorage.getItem("autoSubmit") == "true";
   document.getElementById("auto-submit").checked = autoSubmit;
 
   // Restore the view to show the settings described above
-  var use_fuzzy = localStorage.getItem("use_fuzzy_search") != "false";
+  let use_fuzzy = localStorage.getItem("use_fuzzy_search") != "false";
   document.getElementById("use-fuzzy").checked = use_fuzzy;
+
+  let groups = document.getElementsByClassName("password-store-group");
+  let paths = JSON.parse(localStorage.getItem("paths") || "[]");
+  for (let i = 0; i < paths.length; i++) {
+    let path = paths[i];
+    let group = groups[i];
+    group.querySelector(".password-store-path-enabled").checked = path.enabled;
+    group.querySelector(".password-store-path").value = path.path;
+  }
 }
 
 document.addEventListener("DOMContentLoaded", restore_options);

From 71bc2e2570cef22247aa5155f7e8330f2a3dc4d8 Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Sat, 24 Mar 2018 22:56:49 +0100
Subject: [PATCH 03/35] Send settings to the host app with every call

---
 browserpass.go              |   6 +-
 chrome/background.js        |  14 ++--
 chrome/script.browserify.js | 135 +++++++++++++++++++-----------------
 3 files changed, 83 insertions(+), 72 deletions(-)

diff --git a/browserpass.go b/browserpass.go
index b9b5f64..b44f126 100644
--- a/browserpass.go
+++ b/browserpass.go
@@ -35,8 +35,8 @@ var endianness = binary.LittleEndian
 // which is then passed along with the command over the native messaging api.
 type Config struct {
 	// Manual searches use FuzzySearch if true, GlobSearch otherwise
-	UseFuzzy    bool     `json:"use_fuzzy_search"`
-	Directories []string `json:"directories"`
+	UseFuzzy bool     `json:"use_fuzzy_search"`
+	Paths    []string `json:"paths"`
 }
 
 // msg defines a message sent from a browser extension.
@@ -66,7 +66,7 @@ func Run(stdin io.Reader, stdout io.Writer) error {
 			return err
 		}
 
-		s, err := pass.NewDefaultStore(data.Settings.Directories, data.Settings.UseFuzzy)
+		s, err := pass.NewDefaultStore(data.Settings.Paths, data.Settings.UseFuzzy)
 		if err != nil {
 			return err
 		}
diff --git a/chrome/background.js b/chrome/background.js
index c2bd0ad..c548ca8 100644
--- a/chrome/background.js
+++ b/chrome/background.js
@@ -59,7 +59,7 @@ function onMessage(request, sender, sendResponse) {
   if (request.action == "login") {
     chrome.runtime.sendNativeMessage(
       app,
-      { action: "get", entry: request.entry },
+      { action: "get", entry: request.entry, settings: getSettings() },
       function(response) {
         if (chrome.runtime.lastError) {
           var error = chrome.runtime.lastError.message;
@@ -92,9 +92,7 @@ function onMessage(request, sender, sendResponse) {
   // object that has current settings. Update this as new settings
   // are added (or old ones removed)
   if (request.action == "getSettings") {
-    const use_fuzzy_search =
-      localStorage.getItem("use_fuzzy_search") != "false";
-    sendResponse({ use_fuzzy_search: use_fuzzy_search });
+    sendResponse(getSettings());
   }
 
   // spawn a new tab with pre-provided credentials
@@ -121,6 +119,14 @@ function onMessage(request, sender, sendResponse) {
   }
 }
 
+function getSettings() {
+  const use_fuzzy_search = localStorage.getItem("use_fuzzy_search") != "false";
+  const paths = JSON.parse(localStorage.getItem("paths") || "[]")
+    .filter(path => path.enabled)
+    .map(path => path.path);
+  return { paths: paths, use_fuzzy_search: use_fuzzy_search };
+}
+
 // listener function for authentication interception
 function onAuthRequired(request, requestDetails) {
   // ask the user before sending credentials to a different domain
diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js
index 04d0b41..3f9f8ea 100644
--- a/chrome/script.browserify.js
+++ b/chrome/script.browserify.js
@@ -204,10 +204,10 @@ function searchPassword(_domain, action = "search", useFillOnSubmit = true) {
   // by requesting them from the background script (which has localStorage access
   // to the settings). Then construct the message to send to browserpass and
   // send that via sendNativeMessage.
-  chrome.runtime.sendMessage({ action: "getSettings" }, function(response) {
+  chrome.runtime.sendMessage({ action: "getSettings" }, function(settings) {
     chrome.runtime.sendNativeMessage(
       app,
-      { action: action, domain: _domain, settings: response },
+      { action: action, domain: _domain, settings: settings },
       function(response) {
         if (chrome.runtime.lastError) {
           error = chrome.runtime.lastError.message;
@@ -250,57 +250,59 @@ function getFaviconUrl(domain) {
 function launchURL() {
   var what = this.what;
   var entry = this.entry;
-  chrome.runtime.sendNativeMessage(
-    app,
-    { action: "get", entry: this.entry },
-    function(response) {
-      if (chrome.runtime.lastError) {
-        error = chrome.runtime.lastError.message;
-        m.redraw();
-        return;
-      }
-      // get url from login path if not available in the host app response
-      if (!response.hasOwnProperty("url") || response.url.length == 0) {
-        var parts = entry.split(/\//).reverse();
-        for (var i in parts) {
-          var part = parts[i];
-          var info = Tldjs.parse(part);
-          if (
-            info.isValid &&
-            info.tldExists &&
-            info.domain !== null &&
-            info.hostname === part
-          ) {
-            response.url = part;
-            break;
+  chrome.runtime.sendMessage({ action: "getSettings" }, function(settings) {
+    chrome.runtime.sendNativeMessage(
+      app,
+      { action: "get", entry: entry, settings: settings },
+      function(response) {
+        if (chrome.runtime.lastError) {
+          error = chrome.runtime.lastError.message;
+          m.redraw();
+          return;
+        }
+        // get url from login path if not available in the host app response
+        if (!response.hasOwnProperty("url") || response.url.length == 0) {
+          var parts = entry.split(/\//).reverse();
+          for (var i in parts) {
+            var part = parts[i];
+            var info = Tldjs.parse(part);
+            if (
+              info.isValid &&
+              info.tldExists &&
+              info.domain !== null &&
+              info.hostname === part
+            ) {
+              response.url = part;
+              break;
+            }
           }
         }
+        // if a url is present, then launch a new tab via the background script
+        if (response.hasOwnProperty("url") && response.url.length > 0) {
+          var url = response.url.match(/^([a-z]+:)?\/\//i)
+            ? response.url
+            : "http://" + response.url;
+          chrome.runtime.sendMessage({
+            action: "launch",
+            url: url,
+            username: response.u,
+            password: response.p
+          });
+          window.close();
+          return;
+        }
+        // no url available
+        if (!response.hasOwnProperty("url")) {
+          resetWithError(
+            "Unable to determine the URL for this entry. If you have defined one in the password file, " +
+              "your host application must be at least v2.0.14 for this to be usable."
+          );
+        } else {
+          resetWithError("Unable to determine the URL for this entry.");
+        }
       }
-      // if a url is present, then launch a new tab via the background script
-      if (response.hasOwnProperty("url") && response.url.length > 0) {
-        var url = response.url.match(/^([a-z]+:)?\/\//i)
-          ? response.url
-          : "http://" + response.url;
-        chrome.runtime.sendMessage({
-          action: "launch",
-          url: url,
-          username: response.u,
-          password: response.p
-        });
-        window.close();
-        return;
-      }
-      // no url available
-      if (!response.hasOwnProperty("url")) {
-        resetWithError(
-          "Unable to determine the URL for this entry. If you have defined one in the password file, " +
-            "your host application must be at least v2.0.14 for this to be usable."
-        );
-      } else {
-        resetWithError("Unable to determine the URL for this entry.");
-      }
-    }
-  );
+    );
+  });
 }
 
 function getLoginData() {
@@ -326,23 +328,26 @@ function getLoginData() {
 
 function loginToClipboard() {
   var what = this.what;
-  chrome.runtime.sendNativeMessage(
-    app,
-    { action: "get", entry: this.entry },
-    function(response) {
-      if (chrome.runtime.lastError) {
-        error = chrome.runtime.lastError.message;
-        m.redraw();
-      } else {
-        if (what === "password") {
-          toClipboard(response.p);
-        } else if (what === "username") {
-          toClipboard(response.u);
+  var entry = this.entry;
+  chrome.runtime.sendMessage({ action: "getSettings" }, function(settings) {
+    chrome.runtime.sendNativeMessage(
+      app,
+      { action: "get", entry: entry, settings: settings },
+      function(response) {
+        if (chrome.runtime.lastError) {
+          error = chrome.runtime.lastError.message;
+          m.redraw();
+        } else {
+          if (what === "password") {
+            toClipboard(response.p);
+          } else if (what === "username") {
+            toClipboard(response.u);
+          }
+          window.close();
         }
-        window.close();
       }
-    }
-  );
+    );
+  });
 }
 
 function toClipboard(s) {

From 66a5f81f5175ab2a64b0ec3102caad19d5744be8 Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Sat, 24 Mar 2018 23:01:37 +0100
Subject: [PATCH 04/35] Small style improvements on options dialog

---
 chrome/options.html | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/chrome/options.html b/chrome/options.html
index 824714d..c2b5af6 100644
--- a/chrome/options.html
+++ b/chrome/options.html
@@ -4,6 +4,9 @@
   <title>Browserpass Chrome extension for zx2c4's pass (password manager)</title>
   <style>
     body: { padding: 10px; }
+    .password-store-group { margin: 10px 0; }
+    .password-store-path { width: 90%; }
+    #save { margin-top: 20px; }
   </style>
 </head>
 <body>
@@ -43,8 +46,6 @@
   <input type="checkbox" class="password-store-path-enabled" />
   <input type="text" class="password-store-path" />
 </div>
-<br/>
-<br/>
 
 <button id="save">Save</button>
 

From 408b1a307a21cf31214004569e01e5cf261a9ea5 Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Sat, 24 Mar 2018 23:22:48 +0100
Subject: [PATCH 05/35] Fix placeholder value

---
 chrome/options.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/chrome/options.html b/chrome/options.html
index c2b5af6..1fe261e 100644
--- a/chrome/options.html
+++ b/chrome/options.html
@@ -28,7 +28,7 @@
 Custom password stores directories: <br/>
 <div class="password-store-group">
   <input type="checkbox" class="password-store-path-enabled" />
-  <input type="text" class="password-store-path" placeholder="~/.password-store" />
+  <input type="text" class="password-store-path" placeholder="/home/user/.password-store" />
 </div>
 <div class="password-store-group">
   <input type="checkbox" class="password-store-path-enabled" />

From f4d18df1616cb5f5929cfa651bff8b7a02eaa3e1 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 09:36:10 +1300
Subject: [PATCH 06/35] Switch options.js to browserify generation

---
 chrome/options.js | 45 ---------------------------------------------
 makefile          |  5 ++++-
 2 files changed, 4 insertions(+), 46 deletions(-)
 delete mode 100644 chrome/options.js

diff --git a/chrome/options.js b/chrome/options.js
deleted file mode 100644
index d97db69..0000000
--- a/chrome/options.js
+++ /dev/null
@@ -1,45 +0,0 @@
-function save_options() {
-  let autoSubmit = document.getElementById("auto-submit").checked;
-  localStorage.setItem("autoSubmit", autoSubmit);
-
-  // Options related to fuzzy finding.
-  //  use_fuzzy_search indicates if fuzzy finding or glob searching should
-  //  be used in manual searches
-  let use_fuzzy = document.getElementById("use-fuzzy").checked;
-  localStorage.setItem("use_fuzzy_search", use_fuzzy);
-
-  let groups = document.getElementsByClassName("password-store-group");
-  let paths = [];
-  for (let i = 0; i < groups.length; i++) {
-    let group = groups[i];
-    let enabled = group.querySelector(".password-store-path-enabled").checked;
-    let path = group.querySelector(".password-store-path").value;
-    if (path) {
-      paths.push({ path: path, enabled: enabled });
-    }
-  }
-  localStorage.setItem("paths", JSON.stringify(paths));
-
-  window.close();
-}
-
-function restore_options() {
-  let autoSubmit = localStorage.getItem("autoSubmit") == "true";
-  document.getElementById("auto-submit").checked = autoSubmit;
-
-  // Restore the view to show the settings described above
-  let use_fuzzy = localStorage.getItem("use_fuzzy_search") != "false";
-  document.getElementById("use-fuzzy").checked = use_fuzzy;
-
-  let groups = document.getElementsByClassName("password-store-group");
-  let paths = JSON.parse(localStorage.getItem("paths") || "[]");
-  for (let i = 0; i < paths.length; i++) {
-    let path = paths[i];
-    let group = groups[i];
-    group.querySelector(".password-store-path-enabled").checked = path.enabled;
-    group.querySelector(".password-store-path").value = path.path;
-  }
-}
-
-document.addEventListener("DOMContentLoaded", restore_options);
-document.getElementById("save").addEventListener("click", save_options);
diff --git a/makefile b/makefile
index e9b8b72..a5c1c85 100644
--- a/makefile
+++ b/makefile
@@ -1,7 +1,7 @@
 SHELL := /usr/bin/env bash
 CHROME := $(shell which google-chrome 2>/dev/null || which google-chrome-stable 2>/dev/null || which chromium 2>/dev/null || which chromium-browser 2>/dev/null || which chrome 2>/dev/null)
 PEM := $(shell find . -maxdepth 1 -name "*.pem")
-JS_OUTPUT := chrome/script.js chrome/inject.js chrome/inject_otp.js
+JS_OUTPUT := chrome/script.js chrome/options.js chrome/inject.js chrome/inject_otp.js
 BROWSERIFY := ./node_modules/.bin/browserify
 
 all: deps prettier js browserpass
@@ -31,6 +31,9 @@ js: $(JS_OUTPUT)
 chrome/script.js: chrome/script.browserify.js
 	$(BROWSERIFY) chrome/script.browserify.js -o chrome/script.js
 
+chrome/options.js: chrome/options.browserify.js
+	$(BROWSERIFY) chrome/options.browserify.js -o chrome/options.js
+
 browserpass: cmd/browserpass/ pass/ browserpass.go
 	go build -o $@ ./cmd/browserpass
 

From 34af039b6d498e7fa54e397a1ac38942296e7471 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 11:36:40 +1300
Subject: [PATCH 07/35] Rewrite options screen

---
 .gitignore                   |   1 +
 chrome/options.browserify.js | 121 +++++++++++++++++++++++++++++++++++
 chrome/options.html          |  97 +++++++++++++---------------
 3 files changed, 168 insertions(+), 51 deletions(-)
 create mode 100644 chrome/options.browserify.js

diff --git a/.gitignore b/.gitignore
index 0868f90..1842a42 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ firefox/*
 !firefox/host.json
 !firefox/manifest.json
 chrome/script.js
+chrome/options.js
 node_modules
 .vagrant/
 vendor/
diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js
new file mode 100644
index 0000000..f777878
--- /dev/null
+++ b/chrome/options.browserify.js
@@ -0,0 +1,121 @@
+var dirty = [];
+var settings = {
+  autoSubmit: {
+    type: "checkbox",
+    title: "Automatically submit forms after filling",
+    value: false
+  },
+  use_fuzzy_search: {
+    type: "checkbox",
+    title: "Use fuzzy search",
+    value: true
+  },
+  customStore: {
+    title: "Custom password store locations",
+    value: [{name: "one", enabled: true, name: "fish", path: "/home/fish"}],
+  }
+};
+
+// load settings & create render tree
+loadSettings();
+var tree = {
+  view: function() {
+    var nodes = [m("h3", "Basic Settings")];
+    for (key in settings) {
+      var type = settings[key].type;
+      if (type == "checkbox") {
+        nodes.push(createCheckbox(key, settings[key]));
+      }
+    }
+    nodes.push(m("h3", "Custom Store Locations"));
+    for (key in settings.customStore.value) {
+      nodes.push(createCustomStore(key, settings.customStore.value[key]));
+    }
+    nodes.push(m("a", {
+      onclick: function() {
+        settings.customStore.value.push({enabled: true, name: "", path: ""});
+        saveSetting("customStore");
+      }
+    }, "Add Store"));
+    return nodes;
+  }
+};
+
+// attach tree
+var m = require("mithril");
+m.mount(document.body, tree);
+
+// load settings from local storage
+function loadSettings() {
+  for (key in settings) {
+    var value = localStorage.getItem(key);
+    if (value !== null) {
+      settings[key].value = JSON.parse(value);
+    }
+  }
+}
+
+// save settings to local storage
+function saveSetting(name) {
+  var value = settings[name].value;
+  if (Array.isArray(value)) {
+    value = value.filter(item => item !== null);
+  }
+  value = JSON.stringify(value);
+  localStorage.setItem(name, value);
+}
+
+// create a checkbox option
+function createCheckbox(name, option) {
+  return m("div.option", {class: name}, [
+    m("input[type=checkbox]", {
+      name: name,
+      title: option.title,
+      checked: option.value,
+      onchange: function(e) {
+        settings[name].value = e.target.checked;
+        saveSetting(name);
+      }
+    }),
+    m("label", {for: name}, option.title)
+  ]);
+}
+
+// create a custom store option
+function createCustomStore(key, store) {
+  return m("div.option.custom-store", {class: "store-" + store.name}, [
+    m("input[type=checkbox]", {
+      title: "Whether to enable this password store",
+      checked: store.enabled,
+      onchange: function(e) {
+        store.enabled = e.target.checked;
+        saveSetting("customStore");
+      }
+    }),
+    m("input[type=text].name", {
+      title: "The name for this password store",
+      value: store.name,
+      placeholder: "personal",
+      onchange: function(e) {
+        store.name = e.target.value;
+        saveSetting("customStore");
+      }
+    }),
+    m("input[type=text].path", {
+      title: "The full path to this password store",
+      value: store.path,
+      placeholder: "personal",
+      onchange: function(e) {
+        store.path = e.target.value;
+        saveSetting("customStore");
+      }
+    }),
+    m("a.remove", {
+      title: "Remove this password store",
+      onclick: function() {
+        delete settings.customStore.value[key];
+        saveSetting("customStore");
+      }
+    }, "[X]")
+  ]);
+}
diff --git a/chrome/options.html b/chrome/options.html
index 1fe261e..63ad137 100644
--- a/chrome/options.html
+++ b/chrome/options.html
@@ -1,54 +1,49 @@
 <!DOCTYPE html>
 <html>
-<head>
-  <title>Browserpass Chrome extension for zx2c4's pass (password manager)</title>
-  <style>
-    body: { padding: 10px; }
-    .password-store-group { margin: 10px 0; }
-    .password-store-path { width: 90%; }
-    #save { margin-top: 20px; }
-  </style>
-</head>
-<body>
-
-<label>
-  <input type="checkbox" id="auto-submit" />
-  Auto-Submit login forms
-</label>
-<br/>
-<br/>
-
-<label>
-  <input type="checkbox" id="use-fuzzy" />
-  Use fuzzy search
-</label>
-<br/>
-<br/>
-
-Custom password stores directories: <br/>
-<div class="password-store-group">
-  <input type="checkbox" class="password-store-path-enabled" />
-  <input type="text" class="password-store-path" placeholder="/home/user/.password-store" />
-</div>
-<div class="password-store-group">
-  <input type="checkbox" class="password-store-path-enabled" />
-  <input type="text" class="password-store-path" />
-</div>
-<div class="password-store-group">
-  <input type="checkbox" class="password-store-path-enabled" />
-  <input type="text" class="password-store-path" />
-</div>
-<div class="password-store-group">
-  <input type="checkbox" class="password-store-path-enabled" />
-  <input type="text" class="password-store-path" />
-</div>
-<div class="password-store-group">
-  <input type="checkbox" class="password-store-path-enabled" />
-  <input type="text" class="password-store-path" />
-</div>
-
-<button id="save">Save</button>
-
-<script src="options.js"></script>
-</body>
+  <head>
+    <title>Browserpass Chrome extension for zx2c4's pass (password manager)</title>
+    <style>
+      .option {
+        display: flex;
+        height: 16px;
+        line-height: 16px;
+        margin-bottom: 8px;
+      }
+      .option:last-child {
+        margin-bottom: 0;
+      }
+      .option input[type=checkbox] {
+        height: 12px;
+        margin: 2px 6px 2px 0;
+        padding: 0;
+      }
+      .option.custom-store input[type=text] {
+        border: none;
+        border-bottom: 1px solid #AAA;
+        height: 16px;
+        line-height: 16px;
+        margin: -4px 0 0 0;
+        overflow: hidden;
+        padding: 0;
+        width: 25%;
+      }
+      .option.custom-store input[type=text].path {
+        margin-left: 6px;
+        width: calc(100% - 25% - 42px);
+      }
+      .option.custom-store a.remove {
+        color: #F00;
+        display: block;
+        height: 16px;
+        line-height: 16px;
+        margin: 0;
+        padding: 0;
+        text-decoration: none;
+        width: 16px;
+      }
+    </style>
+  </head>
+  <body>
+    <script src="options.js"></script>
+  </body>
 </html>

From 16b261d1c9b04c0da30bc016d1d58c437bc7d162 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 14:34:20 +1300
Subject: [PATCH 08/35] Properly declare local variables as local

---
 chrome/options.browserify.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js
index f777878..2a0a018 100644
--- a/chrome/options.browserify.js
+++ b/chrome/options.browserify.js
@@ -21,14 +21,14 @@ loadSettings();
 var tree = {
   view: function() {
     var nodes = [m("h3", "Basic Settings")];
-    for (key in settings) {
+    for (var key in settings) {
       var type = settings[key].type;
       if (type == "checkbox") {
         nodes.push(createCheckbox(key, settings[key]));
       }
     }
     nodes.push(m("h3", "Custom Store Locations"));
-    for (key in settings.customStore.value) {
+    for (var key in settings.customStore.value) {
       nodes.push(createCustomStore(key, settings.customStore.value[key]));
     }
     nodes.push(m("a", {
@@ -47,7 +47,7 @@ m.mount(document.body, tree);
 
 // load settings from local storage
 function loadSettings() {
-  for (key in settings) {
+  for (var key in settings) {
     var value = localStorage.getItem(key);
     if (value !== null) {
       settings[key].value = JSON.parse(value);

From a665883a02d622ab5371d3fd7f4e92596e1cdc54 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 14:34:45 +1300
Subject: [PATCH 09/35] Add some spacing

---
 chrome/options.html | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/chrome/options.html b/chrome/options.html
index 63ad137..6f9a164 100644
--- a/chrome/options.html
+++ b/chrome/options.html
@@ -29,14 +29,14 @@
       }
       .option.custom-store input[type=text].path {
         margin-left: 6px;
-        width: calc(100% - 25% - 42px);
+        width: calc(100% - 25% - 48px);
       }
       .option.custom-store a.remove {
         color: #F00;
         display: block;
         height: 16px;
         line-height: 16px;
-        margin: 0;
+        margin: 0 0 0 6px;
         padding: 0;
         text-decoration: none;
         width: 16px;

From 38799f53d3dbf3d286f80f3e4c6dbb5f079be9a3 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 14:37:10 +1300
Subject: [PATCH 10/35] Use getSettings() instead of directly reading from
 local storage

---
 chrome/background.js | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/chrome/background.js b/chrome/background.js
index c548ca8..fbee013 100644
--- a/chrome/background.js
+++ b/chrome/background.js
@@ -12,12 +12,6 @@ chrome.runtime.onInstalled.addListener(onExtensionInstalled);
 // fill login form & submit
 function fillLoginForm(login, tab) {
   const loginParam = JSON.stringify(login);
-  const autoSubmit = localStorage.getItem("autoSubmit");
-  const autoSubmitParam = autoSubmit == "true";
-  if (autoSubmit === null) {
-    localStorage.setItem("autoSubmit", autoSubmitParam);
-  }
-
   chrome.tabs.executeScript(
     tab.id,
     {
@@ -27,7 +21,7 @@ function fillLoginForm(login, tab) {
     function() {
       chrome.tabs.executeScript({
         allFrames: true,
-        code: `browserpassFillForm(${loginParam}, ${autoSubmitParam});`
+        code: `browserpassFillForm(${loginParam}, ${getSettings().autoSubmit});`
       });
     }
   );

From 6259c7c5dd529e7f542489281c95cc93917596e2 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 14:37:48 +1300
Subject: [PATCH 11/35] Define getSettings in a more general manner

---
 chrome/background.js | 32 +++++++++++++++++++++++++++-----
 1 file changed, 27 insertions(+), 5 deletions(-)

diff --git a/chrome/background.js b/chrome/background.js
index fbee013..27ac56e 100644
--- a/chrome/background.js
+++ b/chrome/background.js
@@ -114,11 +114,33 @@ function onMessage(request, sender, sendResponse) {
 }
 
 function getSettings() {
-  const use_fuzzy_search = localStorage.getItem("use_fuzzy_search") != "false";
-  const paths = JSON.parse(localStorage.getItem("paths") || "[]")
-    .filter(path => path.enabled)
-    .map(path => path.path);
-  return { paths: paths, use_fuzzy_search: use_fuzzy_search };
+  // default settings
+  var settings = {
+    autoSubmit: false,
+    use_fuzzy_search: true,
+    customStore: []
+  };
+
+  // load settings from local storage
+  for (var key in settings) {
+    var value = localStorage.getItem(key);
+    if (value !== null) {
+      settings[key] = JSON.parse(value);
+    }
+  }
+
+  // filter custom stores by enabled & path length, and ensure they are named
+  settings.customStore = settings.customStore
+    .filter(store => store.enabled && store.path.length > 0)
+    .map(function(store) {
+      if (!store.name) {
+        store.name = store.path;
+      }
+      return store;
+    })
+    ;
+
+  return settings;
 }
 
 // listener function for authentication interception

From 6d6508ad6101fde5afd7d98a30dc2118c4126836 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 14:38:50 +1300
Subject: [PATCH 12/35] Use a list of named password stores

---
 browserpass.go |  8 +++++---
 pass/disk.go   | 52 ++++++++++++++++++++++++++++++--------------------
 2 files changed, 36 insertions(+), 24 deletions(-)

diff --git a/browserpass.go b/browserpass.go
index b44f126..04752ee 100644
--- a/browserpass.go
+++ b/browserpass.go
@@ -33,10 +33,12 @@ var endianness = binary.LittleEndian
 // The browser extension will look up settings in its localstorage and find
 // which options have been selected by the user, and put them in a JSON object
 // which is then passed along with the command over the native messaging api.
+
+// Config defines the root config structure sent from the browser extension
 type Config struct {
 	// Manual searches use FuzzySearch if true, GlobSearch otherwise
-	UseFuzzy bool     `json:"use_fuzzy_search"`
-	Paths    []string `json:"paths"`
+	UseFuzzy    bool                   `json:"use_fuzzy_search"`
+	CustomStore []pass.StoreDefinition `json:"customStore"`
 }
 
 // msg defines a message sent from a browser extension.
@@ -66,7 +68,7 @@ func Run(stdin io.Reader, stdout io.Writer) error {
 			return err
 		}
 
-		s, err := pass.NewDefaultStore(data.Settings.Paths, data.Settings.UseFuzzy)
+		s, err := pass.NewDefaultStore(data.Settings.CustomStore, data.Settings.UseFuzzy)
 		if err != nil {
 			return err
 		}
diff --git a/pass/disk.go b/pass/disk.go
index 39a9761..edb76e6 100644
--- a/pass/disk.go
+++ b/pass/disk.go
@@ -14,46 +14,51 @@ import (
 	sfuzzy "github.com/sahilm/fuzzy"
 )
 
+// StoreDefinition defines a password store object
+type StoreDefinition struct {
+    Name string `json:"name"`
+    Path string `json:"path"`
+}
+
 type diskStore struct {
-	paths    []string
+	Stores   []StoreDefinition
 	useFuzzy bool // Setting for FuzzySearch or GlobSearch in manual searches
 }
 
-func NewDefaultStore(paths []string, useFuzzy bool) (Store, error) {
-	if paths == nil || len(paths) == 0 {
-		defaultPaths, err := defaultStorePath()
+func NewDefaultStore(stores []StoreDefinition, useFuzzy bool) (Store, error) {
+	if stores == nil || len(stores) == 0 {
+		defaultPath, err := defaultStorePath()
 		if err != nil {
 			return nil, err
 		}
-		paths = defaultPaths
+		stores = []StoreDefinition{{Name: "default", Path: defaultPath}}
 	}
 
 	// Follow symlinks
-	finalPaths := make([]string, len(paths))
-	for i, path := range paths {
-		finalPath, err := filepath.EvalSymlinks(path)
+	for i, store := range stores {
+		finalPath, err := filepath.EvalSymlinks(store.Path)
 		if err != nil {
 			return nil, err
 		}
-		finalPaths[i] = finalPath
+		stores[i].Path = finalPath
 	}
 
-	return &diskStore{finalPaths, useFuzzy}, nil
+	return &diskStore{stores, useFuzzy}, nil
 }
 
-func defaultStorePath() ([]string, error) {
+func defaultStorePath() (string, error) {
 	path := os.Getenv("PASSWORD_STORE_DIR")
 	if path != "" {
-		return []string{path}, nil
+		return path, nil
 	}
 
 	usr, err := user.Current()
 	if err != nil {
-		return nil, err
+		return "", err
 	}
 
 	path = filepath.Join(usr.HomeDir, ".password-store")
-	return []string{path}, nil
+	return path, nil
 }
 
 // Do a search. Will call into the correct algoritm (glob or fuzzy)
@@ -94,13 +99,13 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) {
 
 	items := []string{}
 
-	for _, path := range s.paths {
-		matches, err := zglob.GlobFollowSymlinks(path + "/**/" + query + "*/**/*.gpg")
+	for _, store := range s.Stores {
+		matches, err := zglob.GlobFollowSymlinks(store.Path + "/**/" + query + "*/**/*.gpg")
 		if err != nil {
 			return nil, err
 		}
 
-		matches2, err := zglob.GlobFollowSymlinks(path + "/**/" + query + "*.gpg")
+		matches2, err := zglob.GlobFollowSymlinks(store.Path + "/**/" + query + "*.gpg")
 		if err != nil {
 			return nil, err
 		}
@@ -109,11 +114,11 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) {
 
 		for i, match := range allMatches {
 			// TODO this does not handle identical file names in multiple s.paths
-			item, err := filepath.Rel(path, match)
+			item, err := filepath.Rel(store.Path, match)
 			if err != nil {
 				return nil, err
 			}
-			allMatches[i] = strings.TrimSuffix(item, ".gpg")
+			allMatches[i] = store.Name + ":" + strings.TrimSuffix(item, ".gpg")
 		}
 
 		items = append(items, allMatches...)
@@ -136,8 +141,13 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) {
 }
 
 func (s *diskStore) Open(item string) (io.ReadCloser, error) {
-	for _, path := range s.paths {
-		path := filepath.Join(path, item+".gpg")
+	parts := strings.SplitN(item, ":", 2);
+
+	for _, store := range s.Stores {
+		if (store.Name != parts[0]) {
+			continue;
+		}
+		path := filepath.Join(store.Path, parts[1] + ".gpg")
 		f, err := os.Open(path)
 		if os.IsNotExist(err) {
 			continue

From 049494d3ec8758e79bd27142f1f3bb0529faaa17 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 15:05:05 +1300
Subject: [PATCH 13/35] Multi-store display mode

---
 chrome/script.browserify.js | 13 ++++++++++++-
 chrome/styles.css           | 19 ++++++++++++++++++-
 2 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js
index 3f9f8ea..ccf3b31 100644
--- a/chrome/script.browserify.js
+++ b/chrome/script.browserify.js
@@ -42,6 +42,14 @@ function view() {
           title: "Fill form" + (autoSubmit ? " and submit" : "")
         };
 
+	var store = "default";
+	var name = login;
+	var i;
+	if (i = login.indexOf(':')) {
+	  store = login.substr(0, i);
+	  name = login.substr(++i);
+	}
+
         let faviconUrl = getFaviconUrl(domain);
         if (faviconUrl) {
           selector += ".favicon";
@@ -49,7 +57,10 @@ function view() {
         }
 
         return m("div.entry", [
-          m(selector, options, login),
+          m(selector, options, [
+	    store != "default" ? m("div.store", store) : null,
+	    m("div.name", name)
+	  ]),
           m("button.launch.url", {
             onclick: launchURL.bind({ entry: login, what: "url" }),
             title: "Visit URL",
diff --git a/chrome/styles.css b/chrome/styles.css
index c11e54f..9508024 100644
--- a/chrome/styles.css
+++ b/chrome/styles.css
@@ -82,10 +82,11 @@ body {
 }
 
 .login {
+  display: flex;
   flex: 1;
   -webkit-appearance: none;
   -moz-appearance: none;
-  padding: 12px;
+  padding: 0 0 0 12px;
   cursor: pointer;
   border: 0;
   text-align: left;
@@ -102,6 +103,22 @@ body {
   padding-left: 32px;
 }
 
+.login .store {
+  background-color: #090;
+  border-radius: 4px;
+  color: #FFF;
+  height: calc(100% - 24px);
+  line-height: 100%;
+  margin: 10px 4px 10px 0;
+  padding: 2px 4px;
+}
+
+.login .name {
+  height: 100%;
+  line-height: 100%;
+  padding: 12px 0;
+}
+
 .entry:last-of-type {
   border-bottom: 0;
 }

From d12a9489b01c2dfdcda522d991a3bec1b9a4c471 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 15:16:09 +1300
Subject: [PATCH 14/35] Don't display the store badge if the store name isn't
 present

---
 chrome/script.browserify.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js
index ccf3b31..0322f55 100644
--- a/chrome/script.browserify.js
+++ b/chrome/script.browserify.js
@@ -58,7 +58,7 @@ function view() {
 
         return m("div.entry", [
           m(selector, options, [
-	    store != "default" ? m("div.store", store) : null,
+	    (i > 0 && store != "default") ? m("div.store", store) : null,
 	    m("div.name", name)
 	  ]),
           m("button.launch.url", {

From 1b6df1c58defc6516ed7f2853833009b427888cb Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 15:20:30 +1300
Subject: [PATCH 15/35] Increase the minimum width of the popup by 70px

---
 chrome/styles.css | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/chrome/styles.css b/chrome/styles.css
index 9508024..7e33828 100644
--- a/chrome/styles.css
+++ b/chrome/styles.css
@@ -2,7 +2,7 @@ body {
   padding: 0;
   margin: 0;
   font-family: sans-serif;
-  min-width: 280px;
+  min-width: 350px;
   background: white;
   font-size: 14px;
 }

From ca7e08488946498b9448be863ac6d2e2cb9ba535 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 17:16:28 +1300
Subject: [PATCH 16/35] Firefox UI improvements

---
 chrome/options.browserify.js |  2 +-
 chrome/options.html          | 26 +++++++++++++++++++++++---
 chrome/styles.css            |  1 +
 3 files changed, 25 insertions(+), 4 deletions(-)

diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js
index 2a0a018..8851356 100644
--- a/chrome/options.browserify.js
+++ b/chrome/options.browserify.js
@@ -31,7 +31,7 @@ var tree = {
     for (var key in settings.customStore.value) {
       nodes.push(createCustomStore(key, settings.customStore.value[key]));
     }
-    nodes.push(m("a", {
+    nodes.push(m("button.add-store", {
       onclick: function() {
         settings.customStore.value.push({enabled: true, name: "", path: ""});
         saveSetting("customStore");
diff --git a/chrome/options.html b/chrome/options.html
index 6f9a164..7b56270 100644
--- a/chrome/options.html
+++ b/chrome/options.html
@@ -9,9 +9,6 @@
         line-height: 16px;
         margin-bottom: 8px;
       }
-      .option:last-child {
-        margin-bottom: 0;
-      }
       .option input[type=checkbox] {
         height: 12px;
         margin: 2px 6px 2px 0;
@@ -41,6 +38,29 @@
         text-decoration: none;
         width: 16px;
       }
+      .add-store {
+        margin-top: 8px;
+      }
+      @-moz-document url-prefix() {
+        html, body {
+          box-sizing: border-box;
+          overflow: hidden;
+        }
+        body {
+          background: #FFF;
+          border: 1px solid #000;
+          font-family: sans;
+          margin: 2px;
+          padding: 12px;
+        }
+        .option.custom-store input[type=text] {
+          background: #FFF;
+          margin: 2px;
+        }
+        .option.custom-store a.remove {
+          font-size: 12px;
+        }
+      }
     </style>
   </head>
   <body>
diff --git a/chrome/styles.css b/chrome/styles.css
index 7e33828..bf7b9db 100644
--- a/chrome/styles.css
+++ b/chrome/styles.css
@@ -5,6 +5,7 @@ body {
   min-width: 350px;
   background: white;
   font-size: 14px;
+  overflow-y: hidden;
 }
 
 .search > form {

From 6ac632f3df61546f8d20e12f9cad1aca3159836e Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 17:53:36 +1300
Subject: [PATCH 17/35] Return some error messages from the native host app

---
 browserpass.go              | 30 +++++++++++++++++++++---------
 chrome/script.browserify.js |  7 +++++++
 2 files changed, 28 insertions(+), 9 deletions(-)

diff --git a/browserpass.go b/browserpass.go
index 04752ee..131062b 100644
--- a/browserpass.go
+++ b/browserpass.go
@@ -49,6 +49,18 @@ type msg struct {
 	Entry    string `json:"entry"`
 }
 
+func SendError(err error, stdout io.Writer) error {
+	var buf bytes.Buffer
+	if writeError := json.NewEncoder(&buf).Encode(err.Error()); writeError != nil {
+		return err
+	}
+	if writeError := binary.Write(stdout, endianness, uint32(buf.Len())); writeError != nil {
+		return err
+	}
+	buf.WriteTo(stdout)
+	return err
+}
+
 // Run starts browserpass.
 func Run(stdin io.Reader, stdout io.Writer) error {
 	protector.Protect("stdio rpath proc exec")
@@ -58,19 +70,19 @@ func Run(stdin io.Reader, stdout io.Writer) error {
 		if err := binary.Read(stdin, endianness, &n); err == io.EOF {
 			return nil
 		} else if err != nil {
-			return err
+			return SendError(err, stdout)
 		}
 
 		// Get message body
 		var data msg
 		lr := &io.LimitedReader{R: stdin, N: int64(n)}
 		if err := json.NewDecoder(lr).Decode(&data); err != nil {
-			return err
+			return SendError(err, stdout)
 		}
 
 		s, err := pass.NewDefaultStore(data.Settings.CustomStore, data.Settings.UseFuzzy)
 		if err != nil {
-			return err
+			return SendError(err, stdout)
 		}
 
 		var resp interface{}
@@ -78,36 +90,36 @@ func Run(stdin io.Reader, stdout io.Writer) error {
 		case "search":
 			list, err := s.Search(data.Domain)
 			if err != nil {
-				return err
+				return SendError(err, stdout)
 			}
 			resp = list
 		case "match_domain":
 			list, err := s.GlobSearch(data.Domain)
 			if err != nil {
-				return err
+				return SendError(err, stdout)
 			}
 			resp = list
 		case "get":
 			rc, err := s.Open(data.Entry)
 			if err != nil {
-				return err
+				return SendError(err, stdout)
 			}
 			defer rc.Close()
 			login, err := readLoginGPG(rc)
 			if err != nil {
-				return err
+				return SendError(err, stdout)
 			}
 			if login.Username == "" {
 				login.Username = guessUsername(data.Entry)
 			}
 			resp = login
 		default:
-			return errors.New("Invalid action")
+			return SendError(errors.New("Invalid action"), stdout)
 		}
 
 		var b bytes.Buffer
 		if err := json.NewEncoder(&b).Encode(resp); err != nil {
-			return err
+			return SendError(err, stdout)
 		}
 
 		if err := binary.Write(stdout, endianness, uint32(b.Len())); err != nil {
diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js
index 0322f55..b3295c5 100644
--- a/chrome/script.browserify.js
+++ b/chrome/script.browserify.js
@@ -226,6 +226,13 @@ function searchPassword(_domain, action = "search", useFillOnSubmit = true) {
         }
 
         searching = false;
+
+        if (typeof(response) == "string") {
+          error = response;
+          m.redraw();
+          return;
+        }
+
         logins = resultLogins = response ? response : [];
         document.getElementById("filter-search").textContent = domain;
         fillOnSubmit = useFillOnSubmit && logins.length > 0;

From 86a7dede538f3b7b5860b5f6f3d67f592cb1d883 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 18:04:42 +1300
Subject: [PATCH 18/35] Don't search empty or ignored domains

---
 chrome/script.browserify.js | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js
index b3295c5..a17d5ca 100644
--- a/chrome/script.browserify.js
+++ b/chrome/script.browserify.js
@@ -205,6 +205,13 @@ function init(tab) {
 }
 
 function searchPassword(_domain, action = "search", useFillOnSubmit = true) {
+  // don't run searches for empty queries or ignored URLs
+  _domain = _domain.trim();
+  var ignore = ["newtab", "extensions"]
+  if (!_domain.length || ignore.indexOf(_domain) >= 0) {
+    return;
+  }
+
   searching = true;
   logins = resultLogins = [];
   domain = _domain;

From 696ff4aa1f16384b972b4b98f0b51bf86ed70bfb Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 18:18:20 +1300
Subject: [PATCH 19/35] Use settings object from search

---
 chrome/script.browserify.js | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js
index a17d5ca..ac6951f 100644
--- a/chrome/script.browserify.js
+++ b/chrome/script.browserify.js
@@ -11,6 +11,7 @@ var logins = [];
 var fillOnSubmit = false;
 var error;
 var domain, urlDuringSearch;
+var searchSettings;
 
 m.mount(document.getElementById("mount"), { view: view, oncreate: oncreate });
 
@@ -34,12 +35,11 @@ function view() {
         m.trust(`No matching passwords found for <strong>${domain}</strong>.`)
       );
     } else if (logins.length > 0) {
-      const autoSubmit = localStorage.getItem("autoSubmit") == "true";
       results = logins.map(function(login) {
         let selector = "button.login";
         let options = {
           onclick: getLoginData.bind(login),
-          title: "Fill form" + (autoSubmit ? " and submit" : "")
+          title: "Fill form" + (searchSettings.autoSubmit ? " and submit" : "")
         };
 
 	var store = "default";
@@ -223,6 +223,7 @@ function searchPassword(_domain, action = "search", useFillOnSubmit = true) {
   // to the settings). Then construct the message to send to browserpass and
   // send that via sendNativeMessage.
   chrome.runtime.sendMessage({ action: "getSettings" }, function(settings) {
+    searchSettings = settings;
     chrome.runtime.sendNativeMessage(
       app,
       { action: action, domain: _domain, settings: settings },

From c85ad591b5d2cd92f53fcbe7056a95d3d8ba67c8 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 18:18:40 +1300
Subject: [PATCH 20/35] Don't show badges if there is only one custom store
 configured

---
 chrome/script.browserify.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js
index ac6951f..dc8e28e 100644
--- a/chrome/script.browserify.js
+++ b/chrome/script.browserify.js
@@ -46,7 +46,9 @@ function view() {
 	var name = login;
 	var i;
 	if (i = login.indexOf(':')) {
-	  store = login.substr(0, i);
+          if (searchSettings.customStore.length > 1) {
+            store = login.substr(0, i);
+          }
 	  name = login.substr(++i);
 	}
 

From 173e554f57dee3c619df18494cbb0681091e0ccf Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 18:26:46 +1300
Subject: [PATCH 21/35] Remove unused variable

---
 chrome/options.browserify.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js
index 8851356..3d3a093 100644
--- a/chrome/options.browserify.js
+++ b/chrome/options.browserify.js
@@ -1,4 +1,3 @@
-var dirty = [];
 var settings = {
   autoSubmit: {
     type: "checkbox",

From 8c8ca8f5637efe21385c17041199d27eb1da97ec Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 18:40:22 +1300
Subject: [PATCH 22/35] Remove prefix when detecting the URL from the path

---
 chrome/script.browserify.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js
index dc8e28e..4f0e4cc 100644
--- a/chrome/script.browserify.js
+++ b/chrome/script.browserify.js
@@ -290,7 +290,8 @@ function launchURL() {
         }
         // get url from login path if not available in the host app response
         if (!response.hasOwnProperty("url") || response.url.length == 0) {
-          var parts = entry.split(/\//).reverse();
+          var parts = (entry.indexOf(":") > 0) ? entry.substr(entry.indexOf(":") + 1) : entry;
+          parts = parts.split(/\//).reverse();
           for (var i in parts) {
             var part = parts[i];
             var info = Tldjs.parse(part);

From abe6a7cbf6ab087e2b065b7a8bc852711b0c9e33 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 18:46:53 +1300
Subject: [PATCH 23/35] Fix hints

---
 chrome/options.browserify.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js
index 3d3a093..b2b8d8b 100644
--- a/chrome/options.browserify.js
+++ b/chrome/options.browserify.js
@@ -94,7 +94,7 @@ function createCustomStore(key, store) {
     m("input[type=text].name", {
       title: "The name for this password store",
       value: store.name,
-      placeholder: "personal",
+      placeholder: "name",
       onchange: function(e) {
         store.name = e.target.value;
         saveSetting("customStore");
@@ -103,7 +103,7 @@ function createCustomStore(key, store) {
     m("input[type=text].path", {
       title: "The full path to this password store",
       value: store.path,
-      placeholder: "personal",
+      placeholder: "/path/to/store",
       onchange: function(e) {
         store.path = e.target.value;
         saveSetting("customStore");

From af8df7e6945b656197092f5b2e4bc24de5f0d6c8 Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 21:19:16 +1300
Subject: [PATCH 24/35] Remove the fish

---
 chrome/options.browserify.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js
index b2b8d8b..4ee1ac0 100644
--- a/chrome/options.browserify.js
+++ b/chrome/options.browserify.js
@@ -11,7 +11,7 @@ var settings = {
   },
   customStore: {
     title: "Custom password store locations",
-    value: [{name: "one", enabled: true, name: "fish", path: "/home/fish"}],
+    value: [],
   }
 };
 

From c1d750a64bcf615bd127a8fe1af7ee64928a153e Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 21:20:50 +1300
Subject: [PATCH 25/35] Remove TODO comment

---
 pass/disk.go | 1 -
 1 file changed, 1 deletion(-)

diff --git a/pass/disk.go b/pass/disk.go
index edb76e6..7500497 100644
--- a/pass/disk.go
+++ b/pass/disk.go
@@ -113,7 +113,6 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) {
 		allMatches := append(matches, matches2...)
 
 		for i, match := range allMatches {
-			// TODO this does not handle identical file names in multiple s.paths
 			item, err := filepath.Rel(store.Path, match)
 			if err != nil {
 				return nil, err

From 6e9e96ff3df5033d49b3d395b0630795a7de86bc Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 21:22:05 +1300
Subject: [PATCH 26/35] Remove TODO comment

---
 pass/disk.go | 1 -
 1 file changed, 1 deletion(-)

diff --git a/pass/disk.go b/pass/disk.go
index 7500497..7d07060 100644
--- a/pass/disk.go
+++ b/pass/disk.go
@@ -151,7 +151,6 @@ func (s *diskStore) Open(item string) (io.ReadCloser, error) {
 		if os.IsNotExist(err) {
 			continue
 		}
-		// TODO this does not handle identical file names in multiple s.paths
 		return f, err
 	}
 	return nil, errors.New("Unable to find the item on disk")

From d1178ed4a89179ef7f0d01038f29580bf5ca6aba Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 21:34:05 +1300
Subject: [PATCH 27/35] Use plural name for variable

---
 browserpass.go               |  6 +++---
 chrome/background.js         |  4 ++--
 chrome/options.browserify.js | 20 ++++++++++----------
 chrome/script.browserify.js  |  2 +-
 4 files changed, 16 insertions(+), 16 deletions(-)

diff --git a/browserpass.go b/browserpass.go
index 131062b..417de92 100644
--- a/browserpass.go
+++ b/browserpass.go
@@ -37,8 +37,8 @@ var endianness = binary.LittleEndian
 // Config defines the root config structure sent from the browser extension
 type Config struct {
 	// Manual searches use FuzzySearch if true, GlobSearch otherwise
-	UseFuzzy    bool                   `json:"use_fuzzy_search"`
-	CustomStore []pass.StoreDefinition `json:"customStore"`
+	UseFuzzy    bool                    `json:"use_fuzzy_search"`
+	CustomStores []pass.StoreDefinition `json:"customStores"`
 }
 
 // msg defines a message sent from a browser extension.
@@ -80,7 +80,7 @@ func Run(stdin io.Reader, stdout io.Writer) error {
 			return SendError(err, stdout)
 		}
 
-		s, err := pass.NewDefaultStore(data.Settings.CustomStore, data.Settings.UseFuzzy)
+		s, err := pass.NewDefaultStore(data.Settings.CustomStores, data.Settings.UseFuzzy)
 		if err != nil {
 			return SendError(err, stdout)
 		}
diff --git a/chrome/background.js b/chrome/background.js
index 27ac56e..6f6c722 100644
--- a/chrome/background.js
+++ b/chrome/background.js
@@ -118,7 +118,7 @@ function getSettings() {
   var settings = {
     autoSubmit: false,
     use_fuzzy_search: true,
-    customStore: []
+    customStores: []
   };
 
   // load settings from local storage
@@ -130,7 +130,7 @@ function getSettings() {
   }
 
   // filter custom stores by enabled & path length, and ensure they are named
-  settings.customStore = settings.customStore
+  settings.customStores = settings.customStores
     .filter(store => store.enabled && store.path.length > 0)
     .map(function(store) {
       if (!store.name) {
diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js
index 4ee1ac0..6af9af9 100644
--- a/chrome/options.browserify.js
+++ b/chrome/options.browserify.js
@@ -9,7 +9,7 @@ var settings = {
     title: "Use fuzzy search",
     value: true
   },
-  customStore: {
+  customStores: {
     title: "Custom password store locations",
     value: [],
   }
@@ -27,13 +27,13 @@ var tree = {
       }
     }
     nodes.push(m("h3", "Custom Store Locations"));
-    for (var key in settings.customStore.value) {
-      nodes.push(createCustomStore(key, settings.customStore.value[key]));
+    for (var key in settings.customStores.value) {
+      nodes.push(createCustomStore(key, settings.customStores.value[key]));
     }
     nodes.push(m("button.add-store", {
       onclick: function() {
-        settings.customStore.value.push({enabled: true, name: "", path: ""});
-        saveSetting("customStore");
+        settings.customStores.value.push({enabled: true, name: "", path: ""});
+        saveSetting("customStores");
       }
     }, "Add Store"));
     return nodes;
@@ -88,7 +88,7 @@ function createCustomStore(key, store) {
       checked: store.enabled,
       onchange: function(e) {
         store.enabled = e.target.checked;
-        saveSetting("customStore");
+        saveSetting("customStores");
       }
     }),
     m("input[type=text].name", {
@@ -97,7 +97,7 @@ function createCustomStore(key, store) {
       placeholder: "name",
       onchange: function(e) {
         store.name = e.target.value;
-        saveSetting("customStore");
+        saveSetting("customStores");
       }
     }),
     m("input[type=text].path", {
@@ -106,14 +106,14 @@ function createCustomStore(key, store) {
       placeholder: "/path/to/store",
       onchange: function(e) {
         store.path = e.target.value;
-        saveSetting("customStore");
+        saveSetting("customStores");
       }
     }),
     m("a.remove", {
       title: "Remove this password store",
       onclick: function() {
-        delete settings.customStore.value[key];
-        saveSetting("customStore");
+        delete settings.customStores.value[key];
+        saveSetting("customStores");
       }
     }, "[X]")
   ]);
diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js
index 4f0e4cc..7cb8549 100644
--- a/chrome/script.browserify.js
+++ b/chrome/script.browserify.js
@@ -46,7 +46,7 @@ function view() {
 	var name = login;
 	var i;
 	if (i = login.indexOf(':')) {
-          if (searchSettings.customStore.length > 1) {
+          if (searchSettings.customStores.length > 1) {
             store = login.substr(0, i);
           }
 	  name = login.substr(++i);

From f686716ef31fd302040974694cde2a91b31b5a4d Mon Sep 17 00:00:00 2001
From: Erayd <steve@erayd.net>
Date: Tue, 27 Mar 2018 21:45:59 +1300
Subject: [PATCH 28/35] Add default empty entry

---
 chrome/options.browserify.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js
index 6af9af9..a8cd823 100644
--- a/chrome/options.browserify.js
+++ b/chrome/options.browserify.js
@@ -11,7 +11,7 @@ var settings = {
   },
   customStores: {
     title: "Custom password store locations",
-    value: [],
+    value: [{enabled: true, name: "", path: ""}],
   }
 };
 

From 2dea7c0c5d84cbea1d8ba36ec0699b079952e1bc Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Tue, 27 Mar 2018 11:01:36 +0200
Subject: [PATCH 29/35] Resolve conflict with master

---
 cmd/browserpass/main.go | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/cmd/browserpass/main.go b/cmd/browserpass/main.go
index 0345e5e..c34d7c2 100644
--- a/cmd/browserpass/main.go
+++ b/cmd/browserpass/main.go
@@ -1,6 +1,8 @@
 package main
 
 import (
+	"flag"
+	"fmt"
 	"io"
 	"log"
 	"os"
@@ -9,10 +11,19 @@ import (
 	"github.com/dannyvankooten/browserpass/protector"
 )
 
+const VERSION = "2.0.17"
+
 func main() {
 	protector.Protect("stdio rpath proc exec getpw")
 	log.SetPrefix("[Browserpass] ")
 
+	showVersion := flag.Bool("v", false, "print version and exit")
+	flag.Parse()
+	if *showVersion {
+		fmt.Println("Browserpass host app version:", VERSION)
+		os.Exit(0)
+	}
+
 	if err := browserpass.Run(os.Stdin, os.Stdout); err != nil && err != io.EOF {
 		log.Fatal(err)
 	}

From 31b2a0c1fd7bf1cafb51fc5c39214c0aae7eb6b7 Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Tue, 27 Mar 2018 11:19:48 +0200
Subject: [PATCH 30/35] Allow using environment vars and tilde in custom stores

---
 pass/disk.go | 25 +++++++++++++++----------
 1 file changed, 15 insertions(+), 10 deletions(-)

diff --git a/pass/disk.go b/pass/disk.go
index 7d07060..68bd7f2 100644
--- a/pass/disk.go
+++ b/pass/disk.go
@@ -16,8 +16,8 @@ import (
 
 // StoreDefinition defines a password store object
 type StoreDefinition struct {
-    Name string `json:"name"`
-    Path string `json:"path"`
+	Name string `json:"name"`
+	Path string `json:"path"`
 }
 
 type diskStore struct {
@@ -34,13 +34,18 @@ func NewDefaultStore(stores []StoreDefinition, useFuzzy bool) (Store, error) {
 		stores = []StoreDefinition{{Name: "default", Path: defaultPath}}
 	}
 
-	// Follow symlinks
+	// Expand paths, follow symlinks
 	for i, store := range stores {
-		finalPath, err := filepath.EvalSymlinks(store.Path)
+		path := store.Path
+		if strings.HasPrefix(path, "~/") {
+			path = filepath.Join("$HOME", path[2:])
+		}
+		path = os.ExpandEnv(path)
+		path, err := filepath.EvalSymlinks(path)
 		if err != nil {
 			return nil, err
 		}
-		stores[i].Path = finalPath
+		stores[i].Path = path
 	}
 
 	return &diskStore{stores, useFuzzy}, nil
@@ -93,7 +98,7 @@ func (s *diskStore) FuzzySearch(query string) ([]string, error) {
 
 func (s *diskStore) GlobSearch(query string) ([]string, error) {
 	// Search:
-	// 	1. DOMAIN/USERNAME.gpg
+	//	1. DOMAIN/USERNAME.gpg
 	//	2. DOMAIN.gpg
 	//	3. DOMAIN/SUBDIRECTORY/USERNAME.gpg
 
@@ -140,13 +145,13 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) {
 }
 
 func (s *diskStore) Open(item string) (io.ReadCloser, error) {
-	parts := strings.SplitN(item, ":", 2);
+	parts := strings.SplitN(item, ":", 2)
 
 	for _, store := range s.Stores {
-		if (store.Name != parts[0]) {
-			continue;
+		if store.Name != parts[0] {
+			continue
 		}
-		path := filepath.Join(store.Path, parts[1] + ".gpg")
+		path := filepath.Join(store.Path, parts[1]+".gpg")
 		f, err := os.Open(path)
 		if os.IsNotExist(err) {
 			continue

From a9a4617e737ea5c45005ed6b845c25a262632a03 Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Tue, 27 Mar 2018 11:20:40 +0200
Subject: [PATCH 31/35] Run prettier

---
 chrome/background.js         |  3 +--
 chrome/options.browserify.js | 48 +++++++++++++++++++++++-------------
 chrome/script.browserify.js  | 27 +++++++++++---------
 chrome/styles.css            |  2 +-
 4 files changed, 48 insertions(+), 32 deletions(-)

diff --git a/chrome/background.js b/chrome/background.js
index 6f6c722..7c1371e 100644
--- a/chrome/background.js
+++ b/chrome/background.js
@@ -137,8 +137,7 @@ function getSettings() {
         store.name = store.path;
       }
       return store;
-    })
-    ;
+    });
 
   return settings;
 }
diff --git a/chrome/options.browserify.js b/chrome/options.browserify.js
index a8cd823..16801d7 100644
--- a/chrome/options.browserify.js
+++ b/chrome/options.browserify.js
@@ -11,7 +11,7 @@ var settings = {
   },
   customStores: {
     title: "Custom password store locations",
-    value: [{enabled: true, name: "", path: ""}],
+    value: [{ enabled: true, name: "", path: "" }]
   }
 };
 
@@ -30,12 +30,22 @@ var tree = {
     for (var key in settings.customStores.value) {
       nodes.push(createCustomStore(key, settings.customStores.value[key]));
     }
-    nodes.push(m("button.add-store", {
-      onclick: function() {
-        settings.customStores.value.push({enabled: true, name: "", path: ""});
-        saveSetting("customStores");
-      }
-    }, "Add Store"));
+    nodes.push(
+      m(
+        "button.add-store",
+        {
+          onclick: function() {
+            settings.customStores.value.push({
+              enabled: true,
+              name: "",
+              path: ""
+            });
+            saveSetting("customStores");
+          }
+        },
+        "Add Store"
+      )
+    );
     return nodes;
   }
 };
@@ -66,7 +76,7 @@ function saveSetting(name) {
 
 // create a checkbox option
 function createCheckbox(name, option) {
-  return m("div.option", {class: name}, [
+  return m("div.option", { class: name }, [
     m("input[type=checkbox]", {
       name: name,
       title: option.title,
@@ -76,13 +86,13 @@ function createCheckbox(name, option) {
         saveSetting(name);
       }
     }),
-    m("label", {for: name}, option.title)
+    m("label", { for: name }, option.title)
   ]);
 }
 
 // create a custom store option
 function createCustomStore(key, store) {
-  return m("div.option.custom-store", {class: "store-" + store.name}, [
+  return m("div.option.custom-store", { class: "store-" + store.name }, [
     m("input[type=checkbox]", {
       title: "Whether to enable this password store",
       checked: store.enabled,
@@ -109,12 +119,16 @@ function createCustomStore(key, store) {
         saveSetting("customStores");
       }
     }),
-    m("a.remove", {
-      title: "Remove this password store",
-      onclick: function() {
-        delete settings.customStores.value[key];
-        saveSetting("customStores");
-      }
-    }, "[X]")
+    m(
+      "a.remove",
+      {
+        title: "Remove this password store",
+        onclick: function() {
+          delete settings.customStores.value[key];
+          saveSetting("customStores");
+        }
+      },
+      "[X]"
+    )
   ]);
 }
diff --git a/chrome/script.browserify.js b/chrome/script.browserify.js
index 7cb8549..a12e9ca 100644
--- a/chrome/script.browserify.js
+++ b/chrome/script.browserify.js
@@ -42,15 +42,15 @@ function view() {
           title: "Fill form" + (searchSettings.autoSubmit ? " and submit" : "")
         };
 
-	var store = "default";
-	var name = login;
-	var i;
-	if (i = login.indexOf(':')) {
+        var store = "default";
+        var name = login;
+        var i;
+        if ((i = login.indexOf(":"))) {
           if (searchSettings.customStores.length > 1) {
             store = login.substr(0, i);
           }
-	  name = login.substr(++i);
-	}
+          name = login.substr(++i);
+        }
 
         let faviconUrl = getFaviconUrl(domain);
         if (faviconUrl) {
@@ -60,9 +60,9 @@ function view() {
 
         return m("div.entry", [
           m(selector, options, [
-	    (i > 0 && store != "default") ? m("div.store", store) : null,
-	    m("div.name", name)
-	  ]),
+            i > 0 && store != "default" ? m("div.store", store) : null,
+            m("div.name", name)
+          ]),
           m("button.launch.url", {
             onclick: launchURL.bind({ entry: login, what: "url" }),
             title: "Visit URL",
@@ -209,7 +209,7 @@ function init(tab) {
 function searchPassword(_domain, action = "search", useFillOnSubmit = true) {
   // don't run searches for empty queries or ignored URLs
   _domain = _domain.trim();
-  var ignore = ["newtab", "extensions"]
+  var ignore = ["newtab", "extensions"];
   if (!_domain.length || ignore.indexOf(_domain) >= 0) {
     return;
   }
@@ -237,7 +237,7 @@ function searchPassword(_domain, action = "search", useFillOnSubmit = true) {
 
         searching = false;
 
-        if (typeof(response) == "string") {
+        if (typeof response == "string") {
           error = response;
           m.redraw();
           return;
@@ -290,7 +290,10 @@ function launchURL() {
         }
         // get url from login path if not available in the host app response
         if (!response.hasOwnProperty("url") || response.url.length == 0) {
-          var parts = (entry.indexOf(":") > 0) ? entry.substr(entry.indexOf(":") + 1) : entry;
+          var parts =
+            entry.indexOf(":") > 0
+              ? entry.substr(entry.indexOf(":") + 1)
+              : entry;
           parts = parts.split(/\//).reverse();
           for (var i in parts) {
             var part = parts[i];
diff --git a/chrome/styles.css b/chrome/styles.css
index bf7b9db..08c0e91 100644
--- a/chrome/styles.css
+++ b/chrome/styles.css
@@ -107,7 +107,7 @@ body {
 .login .store {
   background-color: #090;
   border-radius: 4px;
-  color: #FFF;
+  color: #fff;
   height: calc(100% - 24px);
   line-height: 100%;
   margin: 10px 4px 10px 0;

From c361f7ba8a733f7dc2fd7b797598eaad457bf664 Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Tue, 27 Mar 2018 12:14:06 +0200
Subject: [PATCH 32/35] Make fuzzy search ignore store name, fix tests

---
 pass/disk.go      | 22 ++++++++------
 pass/disk_test.go | 73 +++++++++++++++++++++--------------------------
 2 files changed, 46 insertions(+), 49 deletions(-)

diff --git a/pass/disk.go b/pass/disk.go
index 68bd7f2..e87f63a 100644
--- a/pass/disk.go
+++ b/pass/disk.go
@@ -21,7 +21,7 @@ type StoreDefinition struct {
 }
 
 type diskStore struct {
-	Stores   []StoreDefinition
+	stores   []StoreDefinition
 	useFuzzy bool // Setting for FuzzySearch or GlobSearch in manual searches
 }
 
@@ -79,21 +79,27 @@ func (s *diskStore) Search(query string) ([]string, error) {
 // for the empty string, then apply appropriate logic to convert results to
 // a slice of strings, finally returning all of the unique entries.
 func (s *diskStore) FuzzySearch(query string) ([]string, error) {
-	var items []string
-	fileList, err := s.GlobSearch("")
+	entries, err := s.GlobSearch("")
 	if err != nil {
 		return nil, err
 	}
 
+	// GlobSearch now results `storename:filename`, for fuzzy search we need to provide only file names
+	var fileNames []string
+	for _, entry := range entries {
+		fileNames = append(fileNames, strings.SplitN(entry, ":", 2)[1])
+	}
+
 	// The resulting match struct does not copy the strings, but rather
 	// provides the index to the original array. Copy those strings
 	// into the result slice
-	matches := sfuzzy.Find(query, fileList)
+	var results []string
+	matches := sfuzzy.Find(query, fileNames)
 	for _, match := range matches {
-		items = append(items, fileList[match.Index])
+		results = append(results, entries[match.Index])
 	}
 
-	return items, nil
+	return results, nil
 }
 
 func (s *diskStore) GlobSearch(query string) ([]string, error) {
@@ -104,7 +110,7 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) {
 
 	items := []string{}
 
-	for _, store := range s.Stores {
+	for _, store := range s.stores {
 		matches, err := zglob.GlobFollowSymlinks(store.Path + "/**/" + query + "*/**/*.gpg")
 		if err != nil {
 			return nil, err
@@ -147,7 +153,7 @@ func (s *diskStore) GlobSearch(query string) ([]string, error) {
 func (s *diskStore) Open(item string) (io.ReadCloser, error) {
 	parts := strings.SplitN(item, ":", 2)
 
-	for _, store := range s.Stores {
+	for _, store := range s.stores {
 		if store.Name != parts[0] {
 			continue
 		}
diff --git a/pass/disk_test.go b/pass/disk_test.go
index 7ea51d8..b8f4b82 100644
--- a/pass/disk_test.go
+++ b/pass/disk_test.go
@@ -8,8 +8,7 @@ import (
 )
 
 func TestDefaultStorePath(t *testing.T) {
-	var home, expectedCustom string
-	var expected, actual []string
+	var home, expectedCustom, expected, actual string
 
 	usr, err := user.Current()
 
@@ -21,44 +20,36 @@ func TestDefaultStorePath(t *testing.T) {
 
 	// default directory
 	os.Setenv("PASSWORD_STORE_DIR", "")
-	expected = []string{filepath.Join(home, ".password-store")}
+	expected = filepath.Join(home, ".password-store")
 	actual, _ = defaultStorePath()
 
-	if len(expected) != len(actual) {
-		t.Errorf("1: '%d' does not match '%d'", len(expected), len(actual))
-	}
-
-	if expected[0] != actual[0] {
-		t.Errorf("2: '%s' does not match '%s'", expected[0], actual[0])
+	if expected != actual {
+		t.Errorf("1: '%s' does not match '%s'", expected, actual)
 	}
 
 	// custom directory from $PASSWORD_STORE_DIR
-	expectedCustom, err = filepath.Abs("browserpass-test")
+	expected, err = filepath.Abs("browserpass-test")
 	if err != nil {
 		t.Error(err)
 	}
-	expected = []string{expectedCustom}
 
 	os.Mkdir(expectedCustom, os.ModePerm)
-	os.Setenv("PASSWORD_STORE_DIR", expectedCustom)
+	os.Setenv("PASSWORD_STORE_DIR", expected)
 	actual, err = defaultStorePath()
 	if err != nil {
 		t.Error(err)
 	}
-	if len(expected) != len(actual) {
-		t.Errorf("3: '%d' does not match '%d'", len(expected), len(actual))
-	}
-	if expected[0] != actual[0] {
-		t.Errorf("4: '%s' does not match '%s'", expected[0], actual[0])
+	if expected != actual {
+		t.Errorf("2: '%s' does not match '%s'", expected, actual)
 	}
 
 	// clean-up
 	os.Setenv("PASSWORD_STORE_DIR", "")
-	os.Remove(expected[0])
+	os.Remove(expected)
 }
 
 func TestDiskStore_Search_nomatch(t *testing.T) {
-	store := diskStore{[]string{"test_store"}, false}
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false}
 
 	domain := "this-most-definitely-does-not-exist"
 	logins, err := store.Search(domain)
@@ -71,8 +62,8 @@ func TestDiskStore_Search_nomatch(t *testing.T) {
 }
 
 func TestDiskStoreSearch(t *testing.T) {
-	store := diskStore{[]string{"test_store"}, false}
-	targetDomain := "abc.com"
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false}
+	expectedResult := "default:abc.com"
 	testDomains := []string{"abc.com", "test.abc.com", "testing.test.abc.com"}
 	for _, domain := range testDomains {
 		searchResults, err := store.Search(domain)
@@ -82,19 +73,19 @@ func TestDiskStoreSearch(t *testing.T) {
 		// check if result contains abc.com
 		found := false
 		for _, searchResult := range searchResults {
-			if searchResult == targetDomain {
+			if searchResult == expectedResult {
 				found = true
 				break
 			}
 		}
 		if found != true {
-			t.Fatalf("Couldn't find %v in %v", targetDomain, searchResults)
+			t.Fatalf("Couldn't find %v in %v", expectedResult, searchResults)
 		}
 	}
 }
 
 func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testing.T) {
-	store := diskStore{[]string{"test_store"}, false}
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false}
 	searchResult, err := store.Search("xyz")
 	if err != nil {
 		t.Fatal(err)
@@ -102,14 +93,14 @@ func TestDiskStoreSearchNoDuplicatesWhenPatternMatchesDomainAndUsername(t *testi
 	if len(searchResult) != 1 {
 		t.Fatalf("Found %v results instead of 1", len(searchResult))
 	}
-	expectedResult := "xyz.com/xyz_user"
+	expectedResult := "default:xyz.com/xyz_user"
 	if searchResult[0] != expectedResult {
 		t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult[0])
 	}
 }
 
 func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) {
-	store := diskStore{[]string{"test_store"}, false}
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false}
 	searchResult, err := store.Search("def.com")
 	if err != nil {
 		t.Fatal(err)
@@ -117,14 +108,14 @@ func TestDiskStoreSearchFollowsSymlinkFiles(t *testing.T) {
 	if len(searchResult) != 1 {
 		t.Fatalf("Found %v results instead of 1", len(searchResult))
 	}
-	expectedResult := "def.com"
+	expectedResult := "default:def.com"
 	if searchResult[0] != expectedResult {
 		t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult[0])
 	}
 }
 
 func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) {
-	store := diskStore{[]string{"test_store"}, false}
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false}
 	searchResult, err := store.Search("amazon.co.uk")
 	if err != nil {
 		t.Fatal(err)
@@ -132,18 +123,18 @@ func TestDiskStoreSearchFollowsSymlinkDirectories(t *testing.T) {
 	if len(searchResult) != 2 {
 		t.Fatalf("Found %v results instead of 2", len(searchResult))
 	}
-	expectedResult := []string{"amazon.co.uk/user1", "amazon.co.uk/user2"}
+	expectedResult := []string{"default:amazon.co.uk/user1", "default:amazon.co.uk/user2"}
 	if searchResult[0] != expectedResult[0] || searchResult[1] != expectedResult[1] {
 		t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult)
 	}
 }
 
 func TestDiskStoreSearchSubDirectories(t *testing.T) {
-	store := diskStore{[]string{"test_store"}, false}
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false}
 	searchTermsMatches := map[string][]string{
-		"abc.org": []string{"abc.org/user3", "abc.org/wiki/user4", "abc.org/wiki/work/user5"},
-		"wiki":    []string{"abc.org/wiki/user4", "abc.org/wiki/work/user5"},
-		"work":    []string{"abc.org/wiki/work/user5"},
+		"abc.org": []string{"default:abc.org/user3", "default:abc.org/wiki/user4", "default:abc.org/wiki/work/user5"},
+		"wiki":    []string{"default:abc.org/wiki/user4", "default:abc.org/wiki/work/user5"},
+		"work":    []string{"default:abc.org/wiki/work/user5"},
 	}
 
 	for term, expectedResult := range searchTermsMatches {
@@ -163,7 +154,7 @@ func TestDiskStoreSearchSubDirectories(t *testing.T) {
 }
 
 func TestDiskStorePartSearch(t *testing.T) {
-	store := diskStore{[]string{"test_store"}, false}
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: false}
 	searchResult, err := store.Search("ab")
 	if err != nil {
 		t.Fatal(err)
@@ -171,7 +162,7 @@ func TestDiskStorePartSearch(t *testing.T) {
 	if len(searchResult) != 4 {
 		t.Fatalf("Found %v results instead of 4", len(searchResult))
 	}
-	expectedResult := []string{"abc.com", "abc.org/user3", "abc.org/wiki/user4", "abc.org/wiki/work/user5"}
+	expectedResult := []string{"default:abc.com", "default:abc.org/user3", "default:abc.org/wiki/user4", "default:abc.org/wiki/work/user5"}
 	for i := 0; i < len(expectedResult); i++ {
 		if searchResult[i] != expectedResult[i] {
 			t.Fatalf("Couldn't find %v, found %v instead", expectedResult, searchResult)
@@ -180,7 +171,7 @@ func TestDiskStorePartSearch(t *testing.T) {
 }
 
 func TestFuzzySearch(t *testing.T) {
-	store := diskStore{[]string{"test_store"}, true}
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: true}
 	searchResult, err := store.Search("amaz2")
 
 	if err != nil {
@@ -191,8 +182,8 @@ func TestFuzzySearch(t *testing.T) {
 	}
 
 	expectedResult := map[string]bool{
-		"amazon.co.uk/user2": true,
-		"amazon.com/user2":   true,
+		"default:amazon.co.uk/user2": true,
+		"default:amazon.com/user2":   true,
 	}
 
 	for _, res := range searchResult {
@@ -203,7 +194,7 @@ func TestFuzzySearch(t *testing.T) {
 }
 
 func TestFuzzySearchNoResult(t *testing.T) {
-	store := diskStore{[]string{"test_store"}, true}
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: true}
 	searchResult, err := store.Search("vvv")
 
 	if err != nil {
@@ -215,7 +206,7 @@ func TestFuzzySearchNoResult(t *testing.T) {
 }
 
 func TestFuzzySearchTopLevelEntries(t *testing.T) {
-	store := diskStore{[]string{"test_store"}, true}
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}}, useFuzzy: true}
 	searchResult, err := store.Search("def")
 
 	if err != nil {
@@ -226,7 +217,7 @@ func TestFuzzySearchTopLevelEntries(t *testing.T) {
 	}
 
 	expectedResult := map[string]bool{
-		"def.com": true,
+		"default:def.com": true,
 	}
 
 	for _, res := range searchResult {

From af637e656eeeece26db09d9fedb83b19edcd1681 Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Tue, 27 Mar 2018 15:18:50 +0200
Subject: [PATCH 33/35] Add unit tests for multiple stores

---
 makefile                      |  2 +-
 pass/disk_test.go             | 30 ++++++++++++++++++++++++++++++
 pass/test_store_2/abc.com.gpg |  0
 3 files changed, 31 insertions(+), 1 deletion(-)
 create mode 100644 pass/test_store_2/abc.com.gpg

diff --git a/makefile b/makefile
index 3581b76..c8c7c8c 100644
--- a/makefile
+++ b/makefile
@@ -20,7 +20,7 @@ endif
 
 .PHONY: prettier
 prettier:
-	"$(PRETTIER)" --write $(PRETTIER_SOURCES)
+	$(PRETTIER) --write $(PRETTIER_SOURCES)
 
 .PHONY: js
 js: $(JS_OUTPUT)
diff --git a/pass/disk_test.go b/pass/disk_test.go
index b8f4b82..dcaadbd 100644
--- a/pass/disk_test.go
+++ b/pass/disk_test.go
@@ -226,3 +226,33 @@ func TestFuzzySearchTopLevelEntries(t *testing.T) {
 		}
 	}
 }
+
+func TestGlobSearchMultipleStores(t *testing.T) {
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}, StoreDefinition{Name: "custom", Path: "test_store_2"}}, useFuzzy: false}
+	searchResults, err := store.Search("abc.com")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(searchResults) != 2 {
+		t.Fatalf("Found %v results instead of 2", len(searchResults))
+	}
+	expectedResults := []string{"custom:abc.com", "default:abc.com"}
+	if searchResults[0] != expectedResults[0] || searchResults[1] != expectedResults[1] {
+		t.Fatalf("Couldn't find %v, found %v instead", expectedResults, searchResults)
+	}
+}
+
+func TestFuzzySearchMultipleStores(t *testing.T) {
+	store := diskStore{stores: []StoreDefinition{StoreDefinition{Name: "default", Path: "test_store"}, StoreDefinition{Name: "custom", Path: "test_store_2"}}, useFuzzy: true}
+	searchResults, err := store.Search("abc.com")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(searchResults) != 2 {
+		t.Fatalf("Found %v results instead of 2", len(searchResults))
+	}
+	expectedResults := []string{"default:abc.com", "custom:abc.com"}
+	if searchResults[0] != expectedResults[0] || searchResults[1] != expectedResults[1] {
+		t.Fatalf("Couldn't find %v, found %v instead", expectedResults, searchResults)
+	}
+}
diff --git a/pass/test_store_2/abc.com.gpg b/pass/test_store_2/abc.com.gpg
new file mode 100644
index 0000000..e69de29

From 53e1cf02671788be7a2b806bedb32f565a0cd755 Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Tue, 27 Mar 2018 15:38:44 +0200
Subject: [PATCH 34/35] Add tests goal to the makefile

---
 makefile | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/makefile b/makefile
index c8c7c8c..322a5ff 100644
--- a/makefile
+++ b/makefile
@@ -6,7 +6,7 @@ BROWSERIFY := node_modules/.bin/browserify
 PRETTIER := node_modules/.bin/prettier
 PRETTIER_SOURCES := $(shell find chrome -maxdepth 1 -name "*.js" -o -name "*.css")
 
-all: deps prettier js browserpass
+all: deps prettier js browserpass tests
 
 .PHONY: crx
 crx:
@@ -53,6 +53,11 @@ browserpass-openbsd64: cmd/browserpass/ pass/ browserpass.go
 browserpass-freebsd64: cmd/browserpass/ pass/ browserpass.go
 	env GOOS=freebsd GOARCH=amd64 go build -o $@ ./cmd/browserpass
 
+.PHONY: tests
+tests:
+	go test
+	go test ./pass
+
 clean:
 	rm -f browserpass
 	rm -f browserpass-*

From f5b3cf9cacfbec042ecf6c0de9a695faa20c9b00 Mon Sep 17 00:00:00 2001
From: Maxim Baz <git@maximbaz.com>
Date: Tue, 27 Mar 2018 15:56:25 +0200
Subject: [PATCH 35/35] I don't follow the math, but this looks better in
 Firefox

---
 chrome/styles.css | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/chrome/styles.css b/chrome/styles.css
index 08c0e91..95e58b2 100644
--- a/chrome/styles.css
+++ b/chrome/styles.css
@@ -108,7 +108,7 @@ body {
   background-color: #090;
   border-radius: 4px;
   color: #fff;
-  height: calc(100% - 24px);
+  height: calc(100% - 22px);
   line-height: 100%;
   margin: 10px 4px 10px 0;
   padding: 2px 4px;