diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9223769 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +swim diff --git a/board.go b/board.go index 7ec88f4..cc8cd26 100644 --- a/board.go +++ b/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) diff --git a/colors.go b/colors.go index 855fba2..4a6035e 100644 --- a/colors.go +++ b/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"] } diff --git a/lane.go b/lane.go index ee66d73..4aa0df8 100644 --- a/lane.go +++ b/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)) diff --git a/main.go b/main.go index 6f206db..cc84d69 100644 --- a/main.go +++ b/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, diff --git a/story.go b/story.go index 050dffb..51032b8 100644 --- a/story.go +++ b/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()} } diff --git a/swim b/swim deleted file mode 100755 index 3264d4f..0000000 Binary files a/swim and /dev/null differ