2
1
Fork 0

Reworks commands and adds deletion of various objects

This commit is contained in:
sloum 2021-03-26 15:54:43 -07:00
parent 67eae4866c
commit a85b782cf0
7 changed files with 336 additions and 80 deletions

2
.gitignore vendored
View File

@ -1,3 +1 @@
swim
*.json
*.swim

122
board.go
View File

@ -41,6 +41,13 @@ func (b *Board) PollForTermSize() {
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].offset = 0
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Draw(b)
} else {
for l := range b.Lanes {
if len(b.Lanes[l].Stories) == 0 {
continue
}
b.Lanes[l].Current = 0
b.Lanes[l].storyOff = 0
}
b.Draw()
}
}
@ -64,9 +71,9 @@ func (b *Board) Run() {
case 'Q':
Quit()
case 'N':
b.CreateLane()
b.CreateLane("")
case 'n':
b.CreateStory()
b.CreateStory("")
case '\n':
b.StoryOpen = true
b.ViewStory()
@ -98,26 +105,28 @@ func (b *Board) SetMessage(msg string, isError bool) {
b.msgErr = isError
}
func (b *Board) CreateLane() {
laneTitle, err := GetAndConfirmCommandLine("Lane Title: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
func (b *Board) CreateLane(name string) {
var err error
if name == "" {
name, err = GetAndConfirmCommandLine("Lane Title: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
}
}
b.Lanes = append(b.Lanes, MakeLane(laneTitle))
b.Lanes = append(b.Lanes, MakeLane(name))
if b.Current < 0 {
b.Current = 0
}
b.SetMessage("Lane created", false)
}
func (b *Board) CreateStory() {
func (b *Board) CreateStory(name string) {
if b.Current < 0 {
b.SetMessage("You must create a lane first", true)
return
}
b.Lanes[b.Current].CreateStory(b)
b.SetMessage("Story created", false)
b.Lanes[b.Current].CreateStory(name, b)
}
func (b Board) PrintHeader() string {
@ -201,6 +210,45 @@ func (b *Board) EnterCommand() {
}
case "q", "quit":
Quit()
case "c", "create":
if len(f) >= 2 {
target := strings.ToLower(f[1])
name := ""
if len(f) > 2 {
name = strings.Join(f[2:], " ")
}
switch target {
case "lane", "l", "la", "lan":
b.CreateLane(name)
case "story", "s", "st", "sto", "stor":
b.CreateStory(name)
case "task", "tas", "ta", "t":
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].AddTask(name, b)
case "comment", "com", "c":
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].AddComment(name, b)
}
}
case "d", "del", "delete":
if len(f) > 1 {
target := strings.ToLower(f[1])
switch target {
case "lane", "l", "la", "lan":
b.DeleteLane()
case "story", "s", "st", "sto", "stor":
b.Lanes[b.Current].DeleteStory(b)
case "t", "ta", "tas", "task":
val := ""
if len(f) > 2 {
val = f[2]
}
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].DeleteTask(val, b)
// TODO
default:
b.SetMessage(fmt.Sprintf("Unknown target %q", f[1]), true)
}
} else {
b.SetMessage("More info needed: 'update [target] [location]'", true)
}
case "set", "s":
if len(f) > 2 {
target := strings.ToLower(f[1])
@ -215,7 +263,7 @@ func (b *Board) EnterCommand() {
b.SetMessage(fmt.Sprintf("Unknown target %q", f[1]), true)
}
} else {
b.SetMessage("More info needed: 'update [target] [location]'", true)
b.SetMessage("More info needed: 'set [target] [location] [[value]]'", true)
}
case "user", "toggle", "t", "u":
b.Lanes[b.Current].Stories[b.Lanes[b.Current].Current].Update(f, b)
@ -228,10 +276,16 @@ func (b *Board) Update(args []string) {
location := strings.ToLower(args[0])
switch location {
case "title", "t", "name", "n":
title, err := GetAndConfirmCommandLine("New board title: ")
if err != nil {
b.SetMessage(err.Error(), true)
break
var title string
var err error
if len(args) == 1 {
title, err = GetAndConfirmCommandLine("New board title: ")
if err != nil {
b.SetMessage(err.Error(), true)
break
}
} else {
title = strings.Join(args[1:], " ")
}
b.Title = title
b.SetMessage("Board title updated", false)
@ -304,7 +358,7 @@ func (b *Board) Move(ch rune) {
// move selection down a story
if b.Lanes[b.Current].Current < len(b.Lanes[b.Current].Stories)-1 {
b.Lanes[b.Current].Current += 1
if b.Lanes[b.Current].Current * 2 + 1 > b.height-5 {
if b.Lanes[b.Current].Current * 3 + 1 > b.height-5 {
b.Lanes[b.Current].storyOff += 1
}
} else {
@ -384,6 +438,40 @@ func (b *Board) Move(ch rune) {
}
}
func (b *Board) DeleteLane() {
if len(b.Lanes) < 1 {
b.SetMessage("There are no lanes to delete", true)
return
}
cont, err := GetConfirmation("Are you sure? Type 'yes' to delete: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
} else if !cont {
b.SetMessage("Deletion canceled", true)
return
}
if len(b.Lanes[b.Current].Stories) > 0 {
cont, err = GetConfirmation("Are you really sure? There are stories in the lane... ")
if err != nil {
b.SetMessage(err.Error(), true)
return
} else if !cont {
b.SetMessage("Deletion canceled", true)
return
}
}
if b.Current == len(b.Lanes)-1 {
b.Lanes = b.Lanes[:len(b.Lanes)-1]
} else {
b.Lanes = append(b.Lanes[:b.Current], b.Lanes[b.Current+1:]...)
}
if b.Current > len(b.Lanes)-1 {
b.Current -= 1
}
b.SetMessage("Lane deleted", false)
}
func (b *Board) ViewStory() {
if b.Current > -1 {
if b.Lanes[b.Current].Current > -1 {

64
lane.go
View File

@ -13,16 +13,21 @@ type Lane struct {
storyOff int // offset for the lane slice
}
func (l *Lane) CreateStory(b *Board) {
storyTitle, err := GetAndConfirmCommandLine("Story Title: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
func (l *Lane) CreateStory(name string, b *Board) {
var err error
if name == "" {
name, err = GetAndConfirmCommandLine("Story Title: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
}
}
l.Stories = append(l.Stories, MakeStory(storyTitle))
l.Stories = append(l.Stories, MakeStory(name))
if l.Current < 0 {
l.Current = 0
}
b.SetMessage("Story created", false)
}
// Zeroes out the offset and current values
@ -55,20 +60,30 @@ func (l Lane) Header(width int, selected bool) string {
func (l Lane) StringSlice(width int, selected bool) []string {
out := make([]string, 0, len(l.Stories) * 3 + 1)
for i := l.storyOff; i < len(l.Stories); i++ {
// Top spacer
out = append(out, fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff))
// First card row
title := l.Stories[i].Title
if len(title) > width-4 {
title = title[:width-5] + "…"
}
leadIn := " "
if selected && l.Current == i {
leadIn = "\033[1m➜ "
}
out = append(out, fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff))
title := l.Stories[i].Title
pts := strconv.Itoa(l.Stories[i].Points)
out = append(out, fmt.Sprintf("%s %s%s%-*.*s%s %s", style.Lane, style.Input, leadIn, width-4, width-4, title, style.Lane, styleOff))
// Second card row
pts := strconv.Itoa(l.Stories[i].Points)
if pts == "0" || pts[0] == '-' {
pts = ""
}
if len(title) > width-4 {
title = title[:width-5] + "…"
users := l.Stories[i].GetUserAbbrString()
if len(users) > width-4 {
users = users[:width-5] + "…"
}
out = append(out, fmt.Sprintf("%s %s%s%-*.*s\033[7m%*s\033[27m%s %s", style.Lane, style.Input, leadIn, width-7, width-7, title, 3, pts, style.Lane, styleOff))
out = append(out, fmt.Sprintf("%s %s %-*.*s\033[7m%*s\033[27m%s %s", style.Lane, style.Input, width-7, width-7, users, 3, pts, style.Lane, styleOff))
}
if len(out) > 0 {
out = append(out, fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff))
@ -76,6 +91,31 @@ func (l Lane) StringSlice(width int, selected bool) []string {
return out
}
func (l *Lane) DeleteStory(b *Board) {
if len(l.Stories) < 1 {
b.SetMessage("There are no stories to delete in this lane", true)
return
}
cont, err := GetConfirmation("Are you sure? Type 'yes' to delete: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
}
if !cont {
b.SetMessage("Deletion canceled", true)
return
}
if l.Current == len(l.Stories)-1 {
l.Stories = l.Stories[:len(l.Stories)-1]
} else {
l.Stories = append(l.Stories[:l.Current], l.Stories[l.Current+1:]...)
}
if l.Current > len(l.Stories)-1 {
l.Current -= 1
}
b.SetMessage("Story deleted", false)
}
func (l *Lane) Update(args []string, b *Board) {
location := strings.ToLower(args[0])
switch location {

17
main.go
View File

@ -73,13 +73,26 @@ func GetLine(prefix string) (string, error) {
}
func GetCommandLine(prefix string) (string, error) {
fmt.Print(upAndLeft) // Move up one and over all
fmt.Print(style.Input)
fmt.Printf("%s%s\033[2K", upAndLeft, style.Input)
line, err := GetLine(prefix)
fmt.Print(cursorEnd)
return line, err
}
func GetConfirmation(prefix string) (bool, error) {
ln, err := GetCommandLine(prefix)
if err != nil {
return false, err
}
ln = strings.ToLower(ln)
switch ln {
case "y", "yes", "yeah", "yup":
return true, nil
default:
return false, nil
}
}
func GetAndConfirmCommandLine(prefix string) (string, error) {
var conf rune
var err error

View File

@ -1,27 +1,76 @@
# swim
Current command list:
A project planning board for the terminal.
[variable-value] [[optional-value]]
:w,
:write,
:wq
[[path]]
## Structs
:q,
:quit
:c,
:create
l,
lane
[[title]]
s,
story,
[[title]]
t,
task,
[[value]]
c,
com,
comment
[[value]]
:s,
:set
b,
board
t,
title
[[value]]
l,
lane
t,
title
[[value]]
s,
story
title
[[value]]
d,
desc,
description
[[value]]
p,
pts,
points
[[point-value]]
u,
user
[[space separated user list to toggle]]
:t,
:toggle
[[task-id]]
:u,
:user
[[space separated user list to toggle]]
:d,
:del,
:delete
l,
lane
s,
story
t,
task
[[task-id]]
1. story
- title string
- body string
- points int
- tag int // enum representing a color
- users []string
- comments []comment
- created time.time // the time the story was created
2. comment
- user string
- body string
- created time.time
3. lane
- title string
- stories []story
4. board
- title string
- body string
- created time.time
- lanes []lane

115
story.go
View File

@ -44,11 +44,13 @@ func (s *Story) View(b *Board) {
case 'Q':
Quit()
case 'c', 'C':
s.AddComment(b)
s.AddComment("", b)
case 't':
s.AddTask(b)
s.AddTask("", b)
case 'd':
s.Update([]string{"description"}, b)
case 'D':
s.DeleteTask("", b)
case 'T':
s.Update([]string{"title"}, b)
case 'u':
@ -65,11 +67,14 @@ func (s *Story) View(b *Board) {
}
}
func (s *Story) AddComment(b *Board) {
comment, err := GetAndConfirmCommandLine("Comment: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
func (s *Story) AddComment(comment string, b *Board) {
var err error
if comment == "" {
comment, err = GetAndConfirmCommandLine("Comment: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
}
}
u, err := user.Current()
if err != nil {
@ -179,24 +184,33 @@ func (s *Story) Scroll(dir rune, b *Board) {
func (s *Story) Update(args []string, b *Board) {
location := strings.ToLower(args[0])
var stringVal string
var err error
if len(args) > 1 {
stringVal = strings.Join(args[1:], " ")
}
switch location {
case "title":
title, err := GetAndConfirmCommandLine("New story title: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
if stringVal == "" {
stringVal, err = GetAndConfirmCommandLine("New story title: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
}
}
s.Title = title
s.Title = stringVal
s.Updated = time.Now()
b.SetMessage("Story title updated", false)
s.BuildStorySlice(b.width)
case "description", "d", "desc", "body", "b":
body, err := GetAndConfirmCommandLine("New story description: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
if stringVal == "" {
stringVal, err = GetAndConfirmCommandLine("New story description: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
}
}
s.Body = body
s.Body = stringVal
s.Updated = time.Now()
b.SetMessage("Story body updated", false)
s.BuildStorySlice(b.width)
@ -241,11 +255,11 @@ func (s *Story) Update(args []string, b *Board) {
args = append(args, n)
}
num, err := strconv.Atoi(args[1])
num -= 1
if err != nil || num < 0 || num >= len(s.Tasks) {
if err != nil || num < 1 || num > len(s.Tasks) {
b.SetMessage("Invalid task number", true)
return
}
num -= 1
s.Tasks[num].Complete = !s.Tasks[num].Complete
s.Updated = time.Now()
b.SetMessage("Task state updated", false)
@ -276,11 +290,14 @@ func (s *Story) AddRemoveUser(users []string, b *Board) {
b.SetMessage("Updated user list", false)
}
func (s *Story) AddTask(b *Board) {
body, err := GetAndConfirmCommandLine("New task: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
func (s *Story) AddTask(body string, b *Board) {
var err error
if body == "" {
body, err = GetAndConfirmCommandLine("New task: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
}
}
s.Tasks = append(s.Tasks, Task{body, false})
s.Updated = time.Now()
@ -288,6 +305,56 @@ func (s *Story) AddTask(b *Board) {
b.SetMessage("Task added", false)
}
func (s *Story) DeleteTask(id string, b *Board) {
var err error
if id == "" {
id, err = GetCommandLine("Task # to delete: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
}
}
num, err := strconv.Atoi(id)
if err != nil || num < 1 || num > len(s.Tasks) {
b.SetMessage("Invalid task number", true)
return
}
num -= 1
var cont bool
cont, err = GetConfirmation("Are you sure? Type 'yes' to delete: ")
if err != nil {
b.SetMessage(err.Error(), true)
return
} else if !cont {
b.SetMessage("Deletion canceled", true)
return
}
if num == len(s.Tasks)-1 {
s.Tasks = s.Tasks[:len(s.Tasks)-1]
} else {
s.Tasks = append(s.Tasks[:num], s.Tasks[num+1:]...)
}
s.Updated = time.Now()
s.BuildStorySlice(b.width)
b.SetMessage("Task deleted", false)
}
func (s Story) GetUserAbbrString() string {
var out strings.Builder
for i := range s.Users {
if len(s.Users[i]) > 1 {
out.WriteString(strings.ToUpper(s.Users[i][:2]))
} else {
out.WriteString(strings.ToUpper(string(s.Users[i][0])))
}
if i < len(s.Users)-1 {
out.WriteString(", ")
}
}
return out.String()
}
func (s Story) Duplicate() Story {
out := Story{}
out.Title = s.Title

1
swim.json Normal file

File diff suppressed because one or more lines are too long