Initial Commit
This commit is contained in:
commit
a1b8560b85
7
README.md
Normal file
7
README.md
Normal file
@ -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.
|
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
||||
module git.bullercodeworks.com/brian/go-projecttxt
|
||||
|
||||
go 1.18
|
115
project.go
Normal file
115
project.go
Normal file
@ -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]
|
||||
}
|
213
projectlist.go
Normal file
213
projectlist.go
Normal file
@ -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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user