Fully movable stories with error messaging for movement
This commit is contained in:
parent
c972b2e2f5
commit
a3bc3814e0
|
@ -0,0 +1 @@
|
|||
swim
|
197
board.go
197
board.go
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
"tildegit.org/sloum/swim/termios"
|
||||
|
@ -22,6 +23,7 @@ type Board struct {
|
|||
Created time.Time
|
||||
Lanes []Lane
|
||||
Current int // Index of current lane
|
||||
LaneOff int
|
||||
Message string
|
||||
MsgErr bool
|
||||
Width int
|
||||
|
@ -41,6 +43,7 @@ func (b *Board) Run() {
|
|||
switch ch {
|
||||
case 'Q':
|
||||
termios.Restore()
|
||||
fmt.Print("\033[?25h")
|
||||
os.Exit(0)
|
||||
case 'N':
|
||||
b.CreateLane()
|
||||
|
@ -48,27 +51,21 @@ func (b *Board) Run() {
|
|||
b.CreateStory()
|
||||
case '\n':
|
||||
// View current story
|
||||
case 'h', 'j', 'k', 'l':
|
||||
// Move cursor, context dependent
|
||||
// If a story is open, will scroll
|
||||
// the story, otherwise will select
|
||||
// a story
|
||||
case 'h', 'j', 'k', 'l', 'H', 'L', 'K', 'J':
|
||||
b.ClearMessage()
|
||||
b.Move(ch)
|
||||
case 'c':
|
||||
// Comment on current story
|
||||
case 'a':
|
||||
// Archive the current story
|
||||
case 'd':
|
||||
// Delete current story
|
||||
case 'D':
|
||||
// Delete current lane
|
||||
// Delete current story
|
||||
case 'e':
|
||||
// Edit current story
|
||||
case ':':
|
||||
b.EnterCommand()
|
||||
case '+':
|
||||
// b.ZoomIn()
|
||||
b.ZoomIn()
|
||||
case '-':
|
||||
// b.ZoomOut()
|
||||
b.ZoomOut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,10 +90,12 @@ func (b *Board) CreateLane() {
|
|||
if b.Current < 0 {
|
||||
b.Current = 0
|
||||
}
|
||||
b.SetMessage("Lane created", false)
|
||||
}
|
||||
|
||||
func (b *Board) CreateStory() {
|
||||
b.Lanes[b.Current].CreateStory(b)
|
||||
b.SetMessage("Story created", false)
|
||||
}
|
||||
|
||||
func (b Board) PrintHeader() string {
|
||||
|
@ -107,38 +106,56 @@ func (b Board) PrintInputArea() string {
|
|||
return fmt.Sprintf("%s%-*.*s%s\n", style.Input, b.Width, b.Width, " ", styleOff)
|
||||
}
|
||||
|
||||
func (b Board) GetLaneSlices(width int) [][]string {
|
||||
laneText := make([][]string, b.Zoom)
|
||||
for i := b.LaneOff; i < b.LaneOff + b.Zoom; i++ {
|
||||
var s []string
|
||||
if i < len(b.Lanes) {
|
||||
s = b.Lanes[i].StringSlice(width, i == b.Current)
|
||||
} else {
|
||||
s = make([]string, 0)
|
||||
}
|
||||
laneText[i] = s
|
||||
}
|
||||
return laneText
|
||||
}
|
||||
|
||||
func (b Board) LaneHeaderRow(width, pad int) string {
|
||||
var out strings.Builder
|
||||
for i := b.LaneOff; i < b.LaneOff + b.Zoom; i++ {
|
||||
if i < len(b.Lanes) {
|
||||
out.WriteString(b.Lanes[i].Header(width, i == b.Current))
|
||||
} else {
|
||||
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff))
|
||||
}
|
||||
}
|
||||
if pad > 0 {
|
||||
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, pad, pad, " ", styleOff))
|
||||
}
|
||||
out.WriteRune('\n')
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func (b Board) PrintLanes() string {
|
||||
var out strings.Builder
|
||||
laneWidth := b.Width / b.Zoom
|
||||
laneSlices := b.GetLaneSlices(laneWidth)
|
||||
pad := b.Width - laneWidth * b.Zoom
|
||||
|
||||
if len(b.Lanes) == 0 {
|
||||
for i := 0; i < b.Height - 3; i++ {
|
||||
out.WriteString(fmt.Sprintf("%s%*.*s%s\n", style.Lane, b.Width, b.Width, " ", styleOff))
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
laneText := make([][]string, 0, len(b.Lanes))
|
||||
maxLen := 0
|
||||
for _, l := range b.Lanes {
|
||||
s := l.StringSlice(laneWidth)
|
||||
laneText = append(laneText, s)
|
||||
if len(s) > maxLen {
|
||||
maxLen = len(s)
|
||||
}
|
||||
}
|
||||
for i := 0; i < b.Height - 3; i++ {
|
||||
for li, l := range laneText {
|
||||
// TODO fix this
|
||||
if li >= b.Zoom {
|
||||
out.WriteRune('\n')
|
||||
break
|
||||
}
|
||||
if i < len(l) {
|
||||
out.WriteString(l[i])
|
||||
out.WriteString(b.LaneHeaderRow(laneWidth, pad))
|
||||
|
||||
for row := 0; row < b.Height - 4; row++ {
|
||||
for _, l := range laneSlices {
|
||||
if row < len(l) {
|
||||
out.WriteString(l[row])
|
||||
} else {
|
||||
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, laneWidth, laneWidth, " ", styleOff))
|
||||
}
|
||||
}
|
||||
if pad > 0 {
|
||||
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, pad, pad, " ", styleOff))
|
||||
}
|
||||
out.WriteRune('\n')
|
||||
}
|
||||
out.WriteString(styleOff)
|
||||
return out.String()
|
||||
|
@ -167,6 +184,114 @@ func (b Board) PrintMessage() string {
|
|||
return out.String()
|
||||
}
|
||||
|
||||
func (b *Board) ZoomIn() {
|
||||
if b.Zoom > 1 {
|
||||
b.Zoom -= 1
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Board) ZoomOut() {
|
||||
if b.Width / (b.Zoom+1) > 15 {
|
||||
b.Zoom += 1
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Board) Move(ch rune) {
|
||||
if len(b.Lanes) == 0 {
|
||||
b.SetMessage("You cannot move what does not exist", true)
|
||||
return
|
||||
}
|
||||
switch ch {
|
||||
case 'h':
|
||||
// move left a lane
|
||||
if b.Current > 0 {
|
||||
b.Current -= 1
|
||||
} else {
|
||||
b.SetMessage("Cannot move further left", true)
|
||||
}
|
||||
case 'l':
|
||||
// move selection right a lane
|
||||
if b.Current < len(b.Lanes)-1 {
|
||||
b.Current += 1
|
||||
} else {
|
||||
b.SetMessage("Cannot move further right", true)
|
||||
}
|
||||
case 'j':
|
||||
// move selection down a story
|
||||
if b.Lanes[b.Current].Current < len(b.Lanes[b.Current].Stories)-1 {
|
||||
b.Lanes[b.Current].Current += 1
|
||||
} else {
|
||||
b.SetMessage("Cannot move further down", true)
|
||||
}
|
||||
case 'k':
|
||||
// move selection up a story
|
||||
if b.Lanes[b.Current].Current > 0 {
|
||||
b.Lanes[b.Current].Current -= 1
|
||||
} else {
|
||||
b.SetMessage("Cannot move further up", true)
|
||||
}
|
||||
case 'H':
|
||||
// move story left a lane
|
||||
if b.Current == 0 {
|
||||
b.SetMessage("Cannot move story left", true)
|
||||
break
|
||||
}
|
||||
storyIndex := b.Lanes[b.Current].Current
|
||||
if len(b.Lanes[b.Current].Stories) <= 0 {
|
||||
goto MoveLeft
|
||||
}
|
||||
b.Lanes[b.Current-1].Stories = append(b.Lanes[b.Current-1].Stories, b.Lanes[b.Current].Stories[storyIndex].Duplicate())
|
||||
if storyIndex == len(b.Lanes[b.Current].Stories)-1 {
|
||||
b.Lanes[b.Current].Stories = b.Lanes[b.Current].Stories[:len(b.Lanes[b.Current].Stories)-1]
|
||||
b.Lanes[b.Current].Current -= 1
|
||||
} else {
|
||||
b.Lanes[b.Current].Stories[storyIndex] = b.Lanes[b.Current].Stories[len(b.Lanes[b.Current].Stories)-1]
|
||||
b.Lanes[b.Current].Stories = b.Lanes[b.Current].Stories[:len(b.Lanes[b.Current].Stories)-1]
|
||||
}
|
||||
b.Lanes[b.Current-1].Current = len(b.Lanes[b.Current-1].Stories)-1
|
||||
MoveLeft: b.Move('h')
|
||||
case 'L':
|
||||
// move story right a lane
|
||||
if b.Current == len(b.Lanes)-1 {
|
||||
b.SetMessage("Cannot move story right", true)
|
||||
break
|
||||
}
|
||||
storyIndex := b.Lanes[b.Current].Current
|
||||
if len(b.Lanes[b.Current].Stories) <= 0 {
|
||||
goto MoveRight
|
||||
}
|
||||
b.Lanes[b.Current+1].Stories = append(b.Lanes[b.Current+1].Stories, b.Lanes[b.Current].Stories[storyIndex].Duplicate())
|
||||
if storyIndex == len(b.Lanes[b.Current].Stories)-1 {
|
||||
b.Lanes[b.Current].Stories = b.Lanes[b.Current].Stories[:len(b.Lanes[b.Current].Stories)-1]
|
||||
b.Lanes[b.Current].Current -= 1
|
||||
} else {
|
||||
b.Lanes[b.Current].Stories[storyIndex] = b.Lanes[b.Current].Stories[len(b.Lanes[b.Current].Stories)-1]
|
||||
b.Lanes[b.Current].Stories = b.Lanes[b.Current].Stories[:len(b.Lanes[b.Current].Stories)-1]
|
||||
}
|
||||
b.Lanes[b.Current+1].Current = len(b.Lanes[b.Current+1].Stories)-1
|
||||
MoveRight: b.Move('l')
|
||||
case 'K':
|
||||
storyIndex := b.Lanes[b.Current].Current
|
||||
if storyIndex <= 0 {
|
||||
b.SetMessage("Cannot move story up", true)
|
||||
break
|
||||
}
|
||||
swapper := reflect.Swapper(b.Lanes[b.Current].Stories)
|
||||
swapper(storyIndex, storyIndex-1)
|
||||
b.Move('k')
|
||||
case 'J':
|
||||
storyIndex := b.Lanes[b.Current].Current
|
||||
if storyIndex > len(b.Lanes[b.Current].Stories)-2 {
|
||||
b.SetMessage("Cannot move story down", true)
|
||||
break
|
||||
}
|
||||
swapper := reflect.Swapper(b.Lanes[b.Current].Stories)
|
||||
swapper(storyIndex, storyIndex+1)
|
||||
b.Move('j')
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (b Board) Draw() {
|
||||
var out strings.Builder
|
||||
out.WriteString(cursorHome)
|
||||
|
|
35
colors.go
35
colors.go
|
@ -7,12 +7,13 @@ const (
|
|||
)
|
||||
|
||||
type Styles struct {
|
||||
Mode int
|
||||
Header string
|
||||
Message string
|
||||
MessageErr string
|
||||
Lane string
|
||||
Input string
|
||||
Mode int
|
||||
Header string
|
||||
Message string
|
||||
MessageErr string
|
||||
Lane string
|
||||
LaneSelected string
|
||||
Input string
|
||||
}
|
||||
|
||||
|
||||
|
@ -24,19 +25,24 @@ var colors = map[int]map[string]string{
|
|||
"Message": "\033[97;42m", // bright white on green
|
||||
"MessageErr": "\033[97;41m", // bright white on red
|
||||
"Lane": "\033[30;104m", // black on bright blue
|
||||
"Input": "\033[30;107m"}, // black on bright white
|
||||
"LaneSelected": "\033[30;103m", // black on bright yellow
|
||||
"Input": "\033[30;107m", // black on bright white
|
||||
},
|
||||
EightBitColor: map[string]string{
|
||||
"Header": "\033[48;5;254m\033[38;5;21\033[1m",
|
||||
"Message": "\033[48;5;35m\033[38;5;231m",
|
||||
"MessageErr": "\033[48;5;124m\033[38;5;231m",
|
||||
"Lane": "\033[48;5;63m\033[38;5;235m",
|
||||
"Input": "\033[48;5;231m\033[38;5;235"},
|
||||
"Input": "\033[48;5;231m\033[38;5;235",
|
||||
},
|
||||
TrueColor: map[string]string{
|
||||
"Header": "",
|
||||
"Message": "",
|
||||
"MessageErr": "",
|
||||
"Lane": "",
|
||||
"Input": ""}}
|
||||
"Input": "",
|
||||
},
|
||||
}
|
||||
|
||||
func (s *Styles) Init(mode int) {
|
||||
if mode == TrueColor || mode == EightBitColor {
|
||||
|
@ -44,9 +50,10 @@ func (s *Styles) Init(mode int) {
|
|||
} else {
|
||||
s.Mode = SimpleColor
|
||||
}
|
||||
s.Header = colors[s.Mode]["Header"]
|
||||
s.Message = colors[s.Mode]["Message"]
|
||||
s.MessageErr = colors[s.Mode]["MessageErr"]
|
||||
s.Lane = colors[s.Mode]["Lane"]
|
||||
s.Input = colors[s.Mode]["Input"]
|
||||
s.Header = colors[s.Mode]["Header"]
|
||||
s.Message = colors[s.Mode]["Message"]
|
||||
s.MessageErr = colors[s.Mode]["MessageErr"]
|
||||
s.Lane = colors[s.Mode]["Lane"]
|
||||
s.LaneSelected = colors[s.Mode]["LaneSelected"]
|
||||
s.Input = colors[s.Mode]["Input"]
|
||||
}
|
||||
|
|
28
lane.go
28
lane.go
|
@ -22,11 +22,33 @@ func (l *Lane) CreateStory(b *Board) {
|
|||
}
|
||||
}
|
||||
|
||||
func (l *Lane) StringSlice(width int) []string {
|
||||
func (l Lane) Header(width int, selected bool) string {
|
||||
marker := " "
|
||||
color := style.Lane
|
||||
if selected {
|
||||
marker = "*"
|
||||
color = style.LaneSelected
|
||||
}
|
||||
if len(l.Title) > width {
|
||||
return fmt.Sprintf("%s\033[1;7m%*.*s%s%s", style.Lane, width, width, l.Title[:width-1], marker, styleOff)
|
||||
} else {
|
||||
width := width - 2
|
||||
leftPad := (width - len(l.Title)) / 2
|
||||
rightPad := width - len(l.Title) - leftPad
|
||||
return fmt.Sprintf("%s %s\033[1;7m%*.*s%s%s%*.*s\033[27m%s %s", style.Lane, color, leftPad-1, leftPad-1, "", l.Title, marker, rightPad, rightPad, "", style.Lane, styleOff)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (l Lane) StringSlice(width int, selected bool) []string {
|
||||
out := make([]string, 0, len(l.Stories) * 3 + 1)
|
||||
for _, story := range l.Stories {
|
||||
for i, story := range l.Stories {
|
||||
leadIn := " "
|
||||
if selected && l.Current == i {
|
||||
leadIn = "\033[1m➜\033[21m "
|
||||
}
|
||||
out = append(out, fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff))
|
||||
out = append(out, fmt.Sprintf("%s %s%-*.*s%s %s", style.Lane, style.Input, width-2, width-2, story.Title, style.Lane, styleOff))
|
||||
out = append(out, fmt.Sprintf("%s %s%s%-*.*s%s %s", style.Lane, style.Input, leadIn, width-4, width-4, story.Title, style.Lane, styleOff))
|
||||
}
|
||||
if len(out) > 0 {
|
||||
out = append(out, fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff))
|
||||
|
|
3
main.go
3
main.go
|
@ -59,11 +59,13 @@ func GetAndConfirmCommandLine(prefix string) (string, error) {
|
|||
if conf == 'y' {
|
||||
break
|
||||
} else if conf == 'n' {
|
||||
fmt.Print(cursorEnd)
|
||||
continue
|
||||
} else if conf == 'c' {
|
||||
err = fmt.Errorf("Cancelled")
|
||||
break
|
||||
} else {
|
||||
fmt.Print(cursorEnd)
|
||||
goto VerifyQuery
|
||||
}
|
||||
}
|
||||
|
@ -79,6 +81,7 @@ func main() {
|
|||
Created: time.Now(),
|
||||
Lanes: make([]Lane, 0, 1),
|
||||
Current: -1,
|
||||
LaneOff: 0,
|
||||
Message: "Welcome to SWIM",
|
||||
MsgErr: false,
|
||||
Width: cols,
|
||||
|
|
16
story.go
16
story.go
|
@ -15,6 +15,22 @@ type Story struct {
|
|||
Updated time.Time
|
||||
}
|
||||
|
||||
func (s Story) Duplicate() Story {
|
||||
out := Story{}
|
||||
out.Title = s.Title
|
||||
out.Body = s.Body
|
||||
out.Users = make([]string, len(s.Users))
|
||||
copy(out.Users, s.Users)
|
||||
out.Tag = s.Tag
|
||||
out.Tasks = make([]Task, len(s.Tasks))
|
||||
copy(out.Tasks, s.Tasks)
|
||||
out.Comments = make([]Comment, len(s.Comments))
|
||||
copy(out.Comments, s.Comments)
|
||||
out.Created = s.Created
|
||||
out.Updated = s.Updated
|
||||
return out
|
||||
}
|
||||
|
||||
func MakeStory(title string) Story {
|
||||
return Story{title,"", make([]string,0,2), -1, make([]Task,0,2), make([]Comment,0,2), time.Now(), time.Now()}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue