commit a1b8560b85d5350c5143b7697916ae939ee0f781 Author: Brian Buller Date: Tue Jun 7 13:50:33 2022 -0500 Initial Commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..b611404 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# go-projecttxt + +A Go library for working with project.txt files. + +A project.txt file is a plain text file for project management purposes. It stems from the same motivations as todo.txt (http://todotxt.org/ ). + +This library is very similar to https://github.com/br0xen/go-todotxt, which is a fork of https://github.com/JamesClonk/go-todotxt. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3a4c459 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.bullercodeworks.com/brian/go-projecttxt + +go 1.18 diff --git a/project.go b/project.go new file mode 100644 index 0000000..1464b71 --- /dev/null +++ b/project.go @@ -0,0 +1,115 @@ +package projecttxt + +import ( + "fmt" + "sort" + "strings" +) + +type Project struct { + Id int // Internal project id + Original string // Original raw project text + Name string // The name of the project + Directory string // The directory part of the project text + Contexts []string + ProjectTags []string + AdditionalTags map[string]string +} + +// ParseProject parses the input text string into a Project struct +func ParseProject(text string) (*Project, error) { + var err error + project := Project{ + AdditionalTags: make(map[string]string), + } + project.Original = strings.Trim(text, "\t\n\r ") + parts := strings.Fields(project.Original) + project.Name, parts = parts[0], parts[1:] + if parts[1][0] == '^' { + project.Directory, parts = parts[0], parts[1:] + } + for _, v := range parts { + switch v[0] { + case '@': // Contexts + project.Contexts = append(project.Contexts, v[1:]) + case '+': // ProjectTags + project.ProjectTags = append(project.ProjectTags, v[1:]) + default: + if strings.Contains(v, ":") { + // Additional Tags + tagPts := strings.Split(v, ":") + if tagPts[0] != "" && tagPts[1] != "" { + project.AdditionalTags[tagPts[0]] = tagPts[1] + } + } + } + } + return &project, err +} + +// String returns a complete project string in project.txt format. +// +// Contexts, ProjectTags, and Additional Tags are alphabetically sorted, +// and appended at the end in the following order: +// Contexts, ProjectTags, Tags +// +// For example: +// Project.txt ^/home/user/projects/projecttxt @personal +projectxt contact:"Frank Workman" "Address 1":"1234 Work Place" +func (project Project) String() string { + text := fmt.Sprintf("%s ", project.Name) + text += fmt.Sprintf("%s ", project.Directory) + if len(project.Contexts) > 0 { + sort.Strings(project.Contexts) + for _, context := range project.Contexts { + text += fmt.Sprintf("@%s ", context) + } + } + if len(project.ProjectTags) > 0 { + sort.Strings(project.ProjectTags) + for _, pTag := range project.ProjectTags { + text += fmt.Sprintf("+%s ", pTag) + } + } + if len(project.AdditionalTags) > 0 { + keys := make([]string, 0, len(project.AdditionalTags)) + for key := range project.AdditionalTags { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + text += fmt.Sprintf("%s:%s ", key, project.AdditionalTags[key]) + } + } + return text +} + +func (project *Project) HasContext(context string) bool { + for _, v := range project.Contexts { + if v == context { + return true + } + } + return false +} + +func (project *Project) HasProjectTag(projectTag string) bool { + for _, v := range project.ProjectTags { + if v == projectTag { + return true + } + } + return false +} + +func (project *Project) SetTag(name, val string) { + project.AdditionalTags[name] = val +} + +func (project *Project) HasTag(name string) bool { + _, ok := project.AdditionalTags[name] + return ok +} + +func (project *Project) GetTag(name string) string { + return project.AdditionalTags[name] +} diff --git a/projectlist.go b/projectlist.go new file mode 100644 index 0000000..3d944eb --- /dev/null +++ b/projectlist.go @@ -0,0 +1,213 @@ +package projecttxt + +import ( + "bufio" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" +) + +type ProjectList []*Project + +func NewProjectList() *ProjectList { + return &ProjectList{} +} + +func (projectlist *ProjectList) Size() int { + return len([]*Project(*projectlist)) +} + +func (projectlist *ProjectList) GetProjectSlice() []*Project { + return []*Project(*projectlist) +} + +func (projectlist *ProjectList) GetProjectsWithContext(context string) *ProjectList { + return projectlist.Filter(func(p *Project) bool { + return p.HasContext(context) + }) +} + +func (projectlist *ProjectList) GetProjectsWithProjectTag(projectTag string) *ProjectList { + return projectlist.Filter(func(p *Project) bool { + return p.HasProjectTag(projectTag) + }) +} + +// Filter filters the current TimerList for the given predicate (a function that takes a timer as input and returns a +// bool), and returns a new ProjectList. The original ProjectList is not modified. +func (projectlist *ProjectList) Filter(predicate func(*Project) bool) *ProjectList { + var newList ProjectList + for _, t := range *projectlist { + if predicate(t) { + newList = append(newList, t) + } + } + return &newList +} + +// AddProject prepends a Project to the current ProjectList and takes care to set the ProjectId correctly +func (projectlist *ProjectList) AddProject(project *Project) { + // The new project will be id 1 + project.Id = 1 + // All other projects get their id incremented + for _, p := range *projectlist { + p.Id++ + } + // Now prepend the project to the slice + *projectlist = append(*projectlist, &Project{}) + copy((*projectlist)[1:], (*projectlist)[0:]) + (*projectlist)[0] = project +} + +// GetProject returns the Project with the given project 'id' form the ProjectList. +// Returns an error if Project could not be found. +func (projectlist *ProjectList) GetProject(id int) (*Project, error) { + for i := range *projectlist { + if ([]*Project(*projectlist))[i].Id == id { + return ([]*Project(*projectlist))[i], nil + } + } + return nil, errors.New("project not found") +} + +// RemoveProjectById removes any Project with given Project 'id' from the ProjectList. +// Returns an error if no Project was removed. +func (projectlist *ProjectList) RemoveProjectById(id int) error { + var newList ProjectList + found := false + for _, p := range *projectlist { + if p.Id != id { + newList = append(newList, p) + } else { + found = true + } + } + if !found { + return errors.New("project not found") + } + *projectlist = newList + return nil +} + +// RemoveProject removes any Project from the ProjectList with the same String representation as the given Project. +// Returns an error if no Project was removed. +func (projectlist *ProjectList) RemoveProject(project Project) error { + var newList ProjectList + found := false + for _, p := range *projectlist { + if p.String() != project.String() { + newList = append(newList, p) + } else { + found = true + } + } + if !found { + return errors.New("project not found") + } + *projectlist = newList + return nil +} + +// ArchiveProjctToFile removes the project from the active list and concatenates it to +// the passed in filename +// Return an err if any part of that fails +func (projectlist *ProjectList) ArchiveProjectToFile(project Project, filename string) error { + if err := projectlist.RemoveProject(project); err != nil { + return err + } + f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600) + if err != nil { + return err + } + defer f.Close() + _, err = f.WriteString(project.String() + "\n") + return err +} + +// LoadFromFile loads a ProjectList from *os.File. +// Note: This will clear the current TimerList and overwrite it's contents with whatever is in *os.File. +func (projectlist *ProjectList) LoadFromFile(file *os.File) error { + *projectlist = []*Project{} // Empty projectlist + projectId := 1 + scanner := bufio.NewScanner(file) + for scanner.Scan() { + text := strings.Trim(scanner.Text(), "\t\n\r") // Read Line + // Ignore blank lines + if text == "" { + continue + } + project, err := ParseProject(text) + if err != nil { + return err + } + project.Id = projectId + *projectlist = append(*projectlist, project) + projectId++ + } + if err := scanner.Err(); err != nil { + return err + } + return nil +} + +// WriteToFile writes a ProjectList to *os.File +func (projectlist *ProjectList) WriteToFile(file *os.File) error { + writer := bufio.NewWriter(file) + _, err := writer.WriteString(projectlist.String()) + writer.Flush() + return err +} + +// LoadFromFilename loads a ProjectList from the filename. +func (projectlist *ProjectList) LoadFromFilename(filename string) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + return projectlist.LoadFromFile(file) +} + +// WriteToFilename writes a ProjectList to the specified file (most likely called "project.txt"). +func (projectlist *ProjectList) WriteToFilename(filename string) error { + return ioutil.WriteFile(filename, []byte(projectlist.String()), 0640) +} + +// String returns a complete list of projects in project.txt format. +func (projectlist ProjectList) String() string { + var ret string + for _, project := range projectlist { + ret += fmt.Sprintf("%s\n", project.String()) + } + return ret +} + +// LoadFromFile loads and returns a ProjectList from *os.File. +func LoadFromFile(file *os.File) (ProjectList, error) { + projectlist := ProjectList{} + if err := projectlist.LoadFromFile(file); err != nil { + return nil, err + } + return projectlist, nil +} + +// WriteToFile writes a ProjectList to *os.File. +func WriteToFile(projectlist *ProjectList, file *os.File) error { + return projectlist.WriteToFile(file) +} + +// LoadFromFilename loads and returns a ProjectList from a file (most likely called "project.txt") +func LoadFromFilename(filename string) (ProjectList, error) { + projectlist := ProjectList{} + if err := projectlist.LoadFromFilename(filename); err != nil { + return nil, err + } + return projectlist, nil +} + +// WriteToFilename write a ProjectList to the specified file (most likely called "project.txt") +func WriteToFilename(projectlist *ProjectList, filename string) error { + return projectlist.WriteToFilename(filename) +}