~psic4t/public-inbox

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch

[PATCH] Refactoring

Details
Message ID
<20221011192950.3520346-1-ser@ser1.net>
DKIM signature
missing
Download raw message
Patch: +232 -248
Removes a lot of dead code; localizes variables that didn't need to be global;
consistent variable names; remove redundant naming (e.g. type thingStruct
struct); generalizes some functions; and general clean-ups.

---
 README.md       |  20 ++++---
 caldavserver.go | 135 +++++++++++++++++++++---------------------------
 defines.go      |  51 +++++++-----------
 directory.go    |  85 +++++++++++-------------------
 go.mod          |   2 +-
 helpers.go      |  64 +++++++++++------------
 main.go         | 117 ++++++++++++++++++++++++++---------------
 parse.go        |   6 +--
 8 files changed, 232 insertions(+), 248 deletions(-)

diff --git a/README.md b/README.md
index e5fcca8..87db3fa 100644
--- a/README.md
+++ b/README.md
@@ -7,16 +7,20 @@ servers / addressbooks in parallel what makes it quite fast.
Its main purpose is displaying addressbook data. Nevertheless it supports basic
creation and editing of entries.

qcard was originally written by ~psic4t on SourceHut, but this fork diverges
significantly.

## Features

- Easy search for contacts
- Parallel fetching of multiple addressbooks 
- Easy to use filters
- Create, modify and delete contacts 
- Import VCF files
- Display VCF files
- Easy setup
- Supports ICS directories, as made by vdirsyncer
- Easy search for contacts.
- Concerrent fetching of multiple addressbooks .
- Easy to use filters.
- Create, modify and delete contacts .
- Import VCF files.
- Display VCF files.
- Easy setup.
- Supports ICS directories, as made by vdirsyncer as well as caldav servers
- Supports arbitrary number of remote and local accounts.

## Installation / Configuration

diff --git a/caldavserver.go b/caldavserver.go
index 9732c40..3eaa675 100644
--- a/caldavserver.go
+++ b/caldavserver.go
@@ -4,6 +4,7 @@ import (
	"bufio"
	"encoding/xml"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
@@ -18,9 +19,7 @@ type caldavserver struct {
	calNo    int
}

func (c caldavserver) getAbProps(receiver chan calProps, errChan chan error) {
	defer close(receiver)
	defer close(errChan)
func (c caldavserver) GetProps() (Properties, error) {
	req, err := http.NewRequest("PROPFIND", c.Url, nil)
	req.SetBasicAuth(c.Username, c.Password)

@@ -28,32 +27,74 @@ func (c caldavserver) getAbProps(receiver chan calProps, errChan chan error) {
	resp, err := cli.Do(req)

	if err != nil {
		errChan <- err
		return
		return Properties{}, err
	}

	xmlContent, _ := ioutil.ReadAll(resp.Body)
	defer resp.Body.Close()

	xmlProps := xmlProps{}
	xmlProps := XmlProps{}
	err = xml.Unmarshal(xmlContent, &xmlProps)
	if err != nil {
		errChan <- err
		return
		return Properties{}, err
	}
	displayName := xmlProps.DisplayName

	thisCal := calProps{
	return Properties{
		calNo:       c.calNo,
		displayName: displayName,
		source:      c.Url,
	}, nil
}

func (c caldavserver) FetchContacts(receiver chan Contact, errChan chan error) {
	defer close(receiver)
	defer close(errChan)
	var xmlBody string

	xmlBody = `<c:addressbook-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav"><d:prop>
            		<d:getetag /><c:address-data />
		   </d:prop></c:addressbook-query>`

	//fmt.Println(xmlBody)
	req, err := http.NewRequest("REPORT", c.Url, strings.NewReader(xmlBody))
	req.SetBasicAuth(c.Username, c.Password)
	req.Header.Add("Content-Type", "application/xml; charset=utf-8")
	req.Header.Add("Depth", "1") // needed for SabreDAV
	req.Header.Add("Prefer", "return-minimal")

	cli := &http.Client{}
	resp, err := cli.Do(req)
	if err != nil {
		errChan <- err
		return
	}

	xmlContent, _ := ioutil.ReadAll(resp.Body)
	defer resp.Body.Close()

	//fmt.Println(string(xmlContent))
	xmlData := XmlData{}
	err = xml.Unmarshal(xmlContent, &xmlData)
	if err != nil {
		errChan <- err
		return
	}

	for i := range xmlData.Elements {
		contactData := xmlData.Elements[i].Data
		contactHref := xmlData.Elements[i].Href
		ABColor := Colors[c.calNo%len(Colors)]
		rv := parseMain(contactData, contactHref, ABColor)
		if rv.fullName != "" {
			receiver <- rv
		}
	}
	receiver <- thisCal
}

func (c caldavserver) deleteContact(contactFilename string) error {
func (c caldavserver) DeleteContact(contactFilename string) error {
	if contactFilename == "" {
		fmt.Errorf("No contact filename given")
		return fmt.Errorf("No contact filename given")
	}

	req, _ := http.NewRequest("DELETE", c.Url+contactFilename, nil)
@@ -61,40 +102,28 @@ func (c caldavserver) deleteContact(contactFilename string) error {

	cli := &http.Client{}
	resp, err := cli.Do(req)
	defer resp.Body.Close()
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return nil
}

func (c caldavserver) dumpContact(contactFilename string, toFile bool) error {
func (c caldavserver) DumpContact(contactFilename string, out io.Writer) error {
	req, _ := http.NewRequest("GET", c.Url+contactFilename, nil)
	req.SetBasicAuth(c.Username, c.Password)

	cli := &http.Client{}
	resp, err := cli.Do(req)
	defer resp.Body.Close()
	if err != nil {
		return err
	}
	//fmt.Println(resp.Status)
	xmlContent, _ := ioutil.ReadAll(resp.Body)

	if toFile {
		// create cache dir if not exists
		os.MkdirAll(cacheLocation, os.ModePerm)
		err := ioutil.WriteFile(cacheLocation+"/"+contactFilename, xmlContent, 0644)
		if err != nil {
			return err
		}
	} else {
		fmt.Println(string(xmlContent))
	}
	defer resp.Body.Close()
	io.Copy(out, resp.Body)
	return nil
}

func (c caldavserver) uploadVCF(contactFilePath string, contactEdit bool) error {
func (c caldavserver) UploadVCF(contactFilePath string, contactEdit bool) error {
	var vcfData string
	var contactVCF string
	var contactFileName string
@@ -109,7 +138,6 @@ func (c caldavserver) uploadVCF(contactFilePath string, contactEdit bool) error
		contactVCF = vcfData
		contactFileName = genUUID() + `.ics`
		fmt.Println(contactVCF)

	} else {
		//eventICS, err := ioutil.ReadFile(cacheLocation + "/" + eventFilename)
		contactVCFByte, err := ioutil.ReadFile(contactFilePath)
@@ -139,52 +167,7 @@ func (c caldavserver) uploadVCF(contactFilePath string, contactEdit bool) error
	return nil
}

func (c caldavserver) fetchAbData(receiver chan contactStruct, errChan chan error) {
	defer close(receiver)
	defer close(errChan)
	var xmlBody string

	xmlBody = `<c:addressbook-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:carddav"><d:prop>
            		<d:getetag /><c:address-data />
		   </d:prop></c:addressbook-query>`

	//fmt.Println(xmlBody)
	req, err := http.NewRequest("REPORT", c.Url, strings.NewReader(xmlBody))
	req.SetBasicAuth(c.Username, c.Password)
	req.Header.Add("Content-Type", "application/xml; charset=utf-8")
	req.Header.Add("Depth", "1") // needed for SabreDAV
	req.Header.Add("Prefer", "return-minimal")

	cli := &http.Client{}
	resp, err := cli.Do(req)
	if err != nil {
		errChan <- err
		return
	}

	xmlContent, _ := ioutil.ReadAll(resp.Body)
	defer resp.Body.Close()

	//fmt.Println(string(xmlContent))
	xmlData := XmlDataStruct{}
	err = xml.Unmarshal(xmlContent, &xmlData)
	if err != nil {
		errChan <- err
		return
	}

	for i := range xmlData.Elements {
		contactData := xmlData.Elements[i].Data
		contactHref := xmlData.Elements[i].Href
		ABColor := Colors[c.calNo%len(Colors)]
		rv := parseMain(contactData, contactHref, ABColor)
		if rv.fullName != "" {
			receiver <- rv
		}
	}
}

func (c caldavserver) createContact(contactData string) error {
func (c caldavserver) CreateContact(contactData string) error {
	str, err := createContact(contactData)
	if err != nil {
		return err
diff --git a/defines.go b/defines.go
index 865b31f..d28abea 100644
--- a/defines.go
+++ b/defines.go
@@ -3,31 +3,19 @@ package main
import (
	_ "embed"
	"encoding/xml"
	"os"
	"time"
	"io"
)

var err string
var homedir string = os.Getenv("HOME")
var editor string = os.Getenv("EDITOR")
var configLocation string = (homedir + "/" + ConfigDir + "/config.json")
var cacheLocation string = (homedir + "/" + CacheDir)
var versionLocation string = (cacheLocation + "/version.json")
var timezone, _ = time.Now().Zone()
var xmlContent []byte
var Version string = "0.6.0"

var showDetails bool
var showFilename bool
var showEmailOnly *bool
var displayFlag bool
var toFile bool
var filter string
var searchterm string

// var colorBlock string = "█"
var colorBlock string = "|"
var Colors = [10]string{"\033[0;31m", "\033[0;32m", "\033[1;33m", "\033[1;34m", "\033[1;35m", "\033[1;36m", "\033[1;37m", "\033[1;38m", "\033[1;39m", "\033[1;40m"}
var showColor bool = true
var qcardversion string = "0.6.0"

const (
	ConfigDir  = ".config/qcard"
@@ -40,22 +28,23 @@ const (
	ColBlue    = "\033[1;34m"
)

type addressBook interface {
	getAbProps(chan calProps, chan error)
	fetchAbData(chan contactStruct, chan error)
	deleteContact(string) error
	dumpContact(string, bool) error
	uploadVCF(string, bool) error
	createContact(string) error
type AddressBook interface {
	// GetProps fetches properties of the address book
	GetProps() (Properties, error)
	FetchContacts(chan Contact, chan error)
	DeleteContact(string) error
	DumpContact(string, io.Writer) error
	UploadVCF(string, bool) error
	CreateContact(string) error
}

type configStruct struct {
	Addressbooks    []addressBook
type Config struct {
	Addressbooks    []AddressBook
	DetailThreshold int
	SortByLastname  bool
}

type contactStruct struct {
type Contact struct {
	Href         string
	Color        string
	fullName     string
@@ -75,7 +64,7 @@ type contactStruct struct {
	note         string
}

type xmlProps struct {
type XmlProps struct {
	calNo        string
	Url          string
	XMLName      xml.Name `xml:"multistatus"`
@@ -87,19 +76,19 @@ type xmlProps struct {
	LastModified string   `xml:"response>propstat>prop>getlastmodified"`
}

type calProps struct {
type Properties struct {
	calNo       int
	displayName string
	source      string
	color       string
}

type XmlDataStruct struct {
type XmlData struct {
	XMLName  xml.Name          `xml:"multistatus"`
	Elements []xmlDataElements `xml:"response"`
	Elements []XmlDataElements `xml:"response"`
}

type xmlDataElements struct {
type XmlDataElements struct {
	XMLName xml.Name `xml:"response"`
	Href    string   `xml:"href"`
	ETag    string   `xml:"propstat>prop>getetag"`
@@ -107,4 +96,4 @@ type xmlDataElements struct {
}

//go:embed "vcard.templ"
var contactSkel string
var ContactSkel string
diff --git a/directory.go b/directory.go
index a131704..264cb72 100644
--- a/directory.go
+++ b/directory.go
@@ -9,31 +9,43 @@ import (
	"path/filepath"
)

type directory struct {
type Directory struct {
	Path  string
	calNo int
}

func (c directory) getAbProps(receiver chan calProps, errChan chan error) {
func (c Directory) GetProps() (Properties, error) {
	return Properties{
		calNo:       c.calNo,
		displayName: filepath.Base(c.Path),
		source:      c.Path,
	}, nil
}

func (c Directory) FetchContacts(receiver chan Contact, errChan chan error) {
	defer close(receiver)
	defer close(errChan)
	ds, err := os.ReadDir(c.Path)
	fmt.Printf("getAbProps have %d contacts\n", len(ds))
	if err != nil {
		errChan <- err
		return
	}
	for i, d := range ds {
		xp := calProps{
			calNo:       i,
			displayName: d.Name(),
			source:      d.Name(),
	for _, file := range ds {
		contactData, err := ioutil.ReadFile(filepath.Join(c.Path, file.Name()))
		if err != nil {
			errChan <- err
			return
		}
		contactHref := file.Name()
		ABColor := Colors[c.calNo%len(Colors)]
		rv := parseMain(string(contactData), contactHref, ABColor)
		if rv.fullName != "" {
			receiver <- rv
		}
		receiver <- xp
	}
}

func (c directory) deleteContact(contactFilename string) error {
func (c Directory) DeleteContact(contactFilename string) error {
	if contactFilename == "" {
		return fmt.Errorf("No contact filename given")
	}
@@ -43,42 +55,30 @@ func (c directory) deleteContact(contactFilename string) error {
	return nil
}

func (c directory) dumpContact(contactFilename string, toFile bool) error {
	xmlContent, err := ioutil.ReadFile(contactFilename)
func (c Directory) DumpContact(contactFilename string, out io.Writer) error {
	file, err := os.Open(contactFilename)
	if err != nil {
		return err
	}

	if toFile {
		// create cache dir if not exists
		os.MkdirAll(cacheLocation, os.ModePerm)
		err := ioutil.WriteFile(cacheLocation+"/"+contactFilename, xmlContent, 0644)
		if err != nil {
			return err
		}
	} else {
		fmt.Println(string(xmlContent))
	}
	defer file.Close()
	io.Copy(out, file)
	return nil
}

func (c directory) uploadVCF(contactFilePath string, contactEdit bool) error {
func (c Directory) UploadVCF(contactFilePath string, contactEdit bool) error {
	var contactFileName string
	var fin io.Reader
	contactFileName = genUUID() + `.ics` // no edit, so new filename

	if contactFilePath == "-" {
		fin = os.Stdin
		contactFileName = genUUID() + `.ics` // no edit, so new filename
	} else {
		var err error
		fin, err = os.Open(contactFilePath)
		if err != nil {
		if fin, err = os.Open(contactFilePath); err != nil {
			return err
		}
		if contactEdit == true {
		if contactEdit {
			contactFileName = path.Base(contactFilePath) // use old filename again
		} else {
			contactFileName = genUUID() + `.ics` // no edit, so new filename
		}
	}
	fout, err := os.Create(filepath.Join(c.Path, contactFileName))
@@ -89,30 +89,7 @@ func (c directory) uploadVCF(contactFilePath string, contactEdit bool) error {
	return err
}

func (c directory) fetchAbData(receiver chan contactStruct, errChan chan error) {
	defer close(receiver)
	defer close(errChan)
	ds, err := os.ReadDir(c.Path)
	if err != nil {
		errChan <- err
		return
	}
	for _, file := range ds {
		contactData, err := ioutil.ReadFile(filepath.Join(c.Path, file.Name()))
		if err != nil {
			errChan <- err
			return
		}
		contactHref := file.Name()
		ABColor := Colors[c.calNo%len(Colors)]
		rv := parseMain(string(contactData), contactHref, ABColor)
		if rv.fullName != "" {
			receiver <- rv
		}
	}
}

func (c directory) createContact(contactData string) error {
func (c Directory) CreateContact(contactData string) error {
	str, err := createContact(contactData)
	if err != nil {
		return err
diff --git a/go.mod b/go.mod
index 6b48968..841ee61 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
module data.haus/qcard
module ser1.net/qcard

go 1.17
diff --git a/helpers.go b/helpers.go
index c22484b..83c2816 100644
--- a/helpers.go
+++ b/helpers.go
@@ -9,18 +9,20 @@ import (
	"log"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"sort"
	"strconv"
	"strings"
	"sync"
	"text/template"
	"time"
)

func getConf() configStruct {
func getConf() Config {
	configLocation := filepath.Join(os.Getenv("HOME"), ConfigDir, "config.json")
	configData, err := ioutil.ReadFile(configLocation)
	if err != nil {
		fmt.Print("Config not found. \n\nPlease copy config-sample.json to ~/.config/qcard/config.json and modify it accordingly.\n\n")
		fmt.Printf("Config not found.\n\nPlease copy config-sample.json to %s and modify it accordingly.\n\n", configLocation)
		log.Fatal(err)
	}
	configMap := make(map[string]interface{})
@@ -29,8 +31,8 @@ func getConf() configStruct {
	if err != nil {
		log.Fatalf("error loading config from %s: %s", configLocation, err)
	}
	conf := configStruct{
		Addressbooks: make([]addressBook, 0),
	conf := Config{
		Addressbooks: make([]AddressBook, 0),
	}
	if dt, ok := configMap["DetailThreshold"].(float64); ok {
		conf.DetailThreshold = int(dt)
@@ -50,7 +52,7 @@ func getConf() configStruct {
						calNo:    ctr,
					})
				} else if m["Path"] != "" {
					conf.Addressbooks = append(conf.Addressbooks, directory{
					conf.Addressbooks = append(conf.Addressbooks, Directory{
						Path:  m["Path"].(string),
						calNo: ctr,
					})
@@ -64,27 +66,24 @@ func getConf() configStruct {
}

func getAbList() {
	props := make(chan calProps, 10)
	errs := make(chan error, 3)
	for _, a := range config.Addressbooks {
		go a.getAbProps(props, errs)
	}

	p := []calProps{}
	for prop := range props {
		p = append(p, prop)
	}
	for err := range errs {
		fmt.Printf("error getting ab list: %v\n", err)
	wg := sync.WaitGroup{}
	printChan := make(chan string, 10)
	for _, ab := range config.Addressbooks {
		wg.Add(1)
		go func(a AddressBook) {
			p, err := a.GetProps()
			if err != nil {
				fmt.Printf("error getting ab list: %v\n", err)
				return
			}
			printChan <- fmt.Sprintf("[%v] - %s%s%s %s (%s)", p.calNo, Colors[p.calNo], colorBlock, ColDefault, p.displayName, p.source)
			wg.Done()
		}(ab)
	}

	sort.Slice(p, func(i, j int) bool {
		return p[i].calNo < (p[j].calNo)
	})

	for i, pi := range p {
		fmt.Printf("[%v] - %s%s%s %s (%s)\n", i, Colors[i], colorBlock, ColDefault, pi.displayName, pi.source)
	for s := range printChan {
		fmt.Println(s)
	}
	wg.Wait()
}

var keyMap map[string]string = map[string]string{
@@ -157,10 +156,7 @@ func splitAfter(s string, re *regexp.Regexp) (r []string) {
	return
}

func (e contactStruct) fancyOutput() {
	//fmt.Println(e.name)
	//fmt.Printf(`%5s`, ` `)
	//fmt.Println(` M: ` + e.phoneCell)
func (e Contact) fancyOutput() {
	if *showEmailOnly {
		showDetails = false

@@ -234,7 +230,7 @@ func (e contactStruct) fancyOutput() {
	}
	//fmt.Println()
}
func (e contactStruct) vcfOutput() {
func (e Contact) vcfOutput() {
	// whole day or greater
	fmt.Println(`Contact
=======`)
@@ -299,13 +295,13 @@ func createContact(contactData string) (string, error) {
	data["uuid"] = genUUID()

	fullName := data["fullName"]
	lastInd := strings.LastIndex(fullName, " ")                      // split name at last space
	name := fullName[lastInd+1:] + ";" + fullName[0:lastInd] + ";;;" // lastname, givenname1 givenname2
	data["name"] = name
	names := strings.Split(fullName, " ")
	namesN := append([]string{names[len(names)-1]}, names[0:len(names)-1]...)
	data["name"] = strings.Join(namesN, ";") // lastname, givenname1 givenname2

	var out strings.Builder
	t := template.New("vcard")
	te, err := t.Parse(contactSkel)
	te, err := t.Parse(ContactSkel)
	if err != nil {
		return "", err
	}
diff --git a/main.go b/main.go
index 69a7311..cf8312c 100644
--- a/main.go
+++ b/main.go
@@ -1,25 +1,31 @@
package main

import (
	// 	"bytes"
	"flag"
	"fmt"
	"log"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	//"strconv"
	"sync"
)

var config configStruct
// TODO Fix the documentatiotn, esp installation
// TODO normalize variable naming and structure
// TODO clean up the rampant global state
// TODO add API documentation
// TODO add unit tests
// TODO the AUR package should copy the conf files to the right plances
// TODO rename package and add communication channels

var config Config

func main() {
	config = getConf()
	toFile := false
	var displayFlag bool

	if len(os.Args[1:]) > 0 {
		searchterm = os.Args[1]
	}
	flag.StringVar(&filter, "s", "", "Search term")
	//flag.BoolVar(&showInfo, "i", false, "Show additional info like description and location for contacts")
	flag.BoolVar(&showFilename, "f", false, "Show contact filename for editing or deletion")
@@ -38,6 +44,9 @@ func main() {
	flagset := make(map[string]bool) // map for flag.Visit. get bools to determine set flags
	flag.Visit(func(f *flag.Flag) { flagset[f.Name] = true })

	cacheLocation := filepath.Join(os.Getenv("HOME"), CacheDir)
	os.MkdirAll(cacheLocation, os.ModePerm)

	// TODO
	if *showAddressbooks {
	}
@@ -52,23 +61,39 @@ func main() {
		}
		ab := config.Addressbooks[*abNo]
		if flagset["delete"] {
			ab.deleteContact(*contactDelete)
			ab.DeleteContact(*contactDelete)
		} else if flagset["d"] {
			ab.dumpContact(*contactDump, toFile)
			// make file here
			// create cache dir if not exists
			var out io.Writer
			if toFile {
				// create cache dir if not exists
				os.MkdirAll(cacheLocation, os.ModePerm)
				fname := filepath.Join(cacheLocation, *contactDump)
				file, err := os.Create(fname)
				if err != nil {
					fmt.Printf("error creating contact file %s: %s\n", fname, err)
					os.Exit(1)
				}
				out = file
			} else {
				out = os.Stdout
			}
			ab.DumpContact(*contactDump, out)
		} else if flagset["p"] {
			displayVCF()
		} else if flagset["n"] {
			ab.createContact(*contactNew)
			ab.CreateContact(*contactNew)
		} else if flagset["u"] {
			contactEdit := false
			ab.uploadVCF(*contactFile, contactEdit)
			ab.UploadVCF(*contactFile, contactEdit)
		} else if flagset["edit"] {
			editContact(ab, *contactEdit)
		}
	} else if flagset["l"] {
		getAbList()
	} else if *version {
		fmt.Println("qcard " + qcardversion)
		fmt.Println("qcard " + Version)
	} else if flagset["a"] {
		showAddresses(*abNo)
	} else {
@@ -77,55 +102,65 @@ func main() {
}

func showAddresses(abn int) {
	abs := make(chan contactStruct, 10)
	ech := make(chan error, 3)
	abs := make(chan Contact, 10)
	ech := make(chan error, 10)
	if abn < 0 {
		for _, ab := range config.Addressbooks {
			go ab.fetchAbData(abs, ech)
			go ab.FetchContacts(abs, ech)
		}
	} else {
		ab := config.Addressbooks[abn]
		go ab.fetchAbData(abs, ech)
		go ab.FetchContacts(abs, ech)
	}

	contactsSlice := make([]contactStruct, 0)
	for cs := range abs {
		contactsSlice = append(contactsSlice, cs)
	}
	for e := range ech {
		fmt.Printf("error processing: %s\n", e)
	}
	contacts := make([]Contact, 0)
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		for c := range abs {
			contacts = append(contacts, c)
		}
		wg.Done()
	}()
	go func() {
		for e := range ech {
			fmt.Printf("error processing: %s\n", e)
		}
		wg.Done()
	}()
	wg.Wait()

	if len(contactsSlice) == 0 {
		log.Fatal("no contacts") // get out if nothing found
	if len(contacts) == 0 {
		return
	}

	// TODO: Allow sort by first and last name
	sort.Slice(contactsSlice, func(i, j int) bool {
	sort.Slice(contacts, func(i, j int) bool {
		if config.SortByLastname {
			return contactsSlice[i].name < contactsSlice[j].name
			return contacts[i].name < contacts[j].name
		} else {
			return contactsSlice[i].fullName < contactsSlice[j].fullName
			return contacts[i].fullName < contacts[j].fullName
		}
	})

	if len(contactsSlice) <= config.DetailThreshold {
		showDetails = true
	}

	if *showEmailOnly {
		fmt.Println("Searching...")
	}
	showDetails = len(contacts) <= config.DetailThreshold

	for _, e := range contactsSlice {
	for _, e := range contacts {
		e.fancyOutput() // pretty print
	}
}

func editContact(c addressBook, contactFilename string) error {
	toFile = true
func editContact(c AddressBook, contactFilename string) error {
	cacheLocation := filepath.Join(os.Getenv("HOME"), CacheDir)
	// create cache dir if not exists
	fname := filepath.Join(cacheLocation, contactFilename)
	toFile, err := os.Create(fname)
	if err != nil {
		fmt.Printf("error creating contact file %s: %s\n", fname, err)
		os.Exit(1)
	}
	contactEdit := true
	c.dumpContact(contactFilename, toFile)
	c.DumpContact(contactFilename, toFile)
	//fmt.Println(appointmentEdit)
	filePath := cacheLocation + "/" + contactFilename
	fileInfo, err := os.Stat(filePath)
@@ -134,7 +169,7 @@ func editContact(c addressBook, contactFilename string) error {
	}
	beforeMTime := fileInfo.ModTime()

	shell := exec.Command(editor, filePath)
	shell := exec.Command(os.Getenv("EDITOR"), filePath)
	shell.Stdout = os.Stdin
	shell.Stdin = os.Stdin
	shell.Stderr = os.Stderr
@@ -147,7 +182,7 @@ func editContact(c addressBook, contactFilename string) error {
	afterMTime := fileInfo.ModTime()

	if beforeMTime.Before(afterMTime) {
		c.uploadVCF(filePath, contactEdit)
		c.UploadVCF(filePath, contactEdit)
	} else {
		return err
	}
diff --git a/parse.go b/parse.go
index f80b97d..695ff31 100644
--- a/parse.go
+++ b/parse.go
@@ -104,12 +104,12 @@ func parseContactNickname(contactData string) string {
	return trimField(result, "(?i)NICKNAME:")
}

func parseMain(contactData, href, color string) contactStruct {
func parseMain(contactData, href, color string) Contact {
	//fmt.Println(parseContactName(contactData))
	fullName := parseContactFullName(contactData)

	if (filter == "") || (filterMatch(fullName) == true) {
		data := contactStruct{
		data := Contact{
			Href:         href,
			Color:        color,
			fullName:     fullName,
@@ -130,5 +130,5 @@ func parseMain(contactData, href, color string) contactStruct {
		}
		return data
	}
	return contactStruct{}
	return Contact{}
}
-- 
2.37.2
Reply to thread Export thread (mbox)