Some screen drawing is happening now but it is janky
This commit is contained in:
parent
4dae95f7d6
commit
c972b2e2f5
|
@ -0,0 +1,179 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"tildegit.org/sloum/swim/termios"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
swimLogo string = "\033[7m swim ▟\033[27m "
|
||||||
|
cursorHome string = "\033[0;0H"
|
||||||
|
styleOff string = "\033[0m"
|
||||||
|
upAndLeft string = "\033[1A\033[500D"
|
||||||
|
cursorEnd string = "\033[500;500H"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Board struct {
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
Created time.Time
|
||||||
|
Lanes []Lane
|
||||||
|
Current int // Index of current lane
|
||||||
|
Message string
|
||||||
|
MsgErr bool
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Zoom int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Board) Run() {
|
||||||
|
defer termios.Restore()
|
||||||
|
termios.SetCharMode()
|
||||||
|
|
||||||
|
var ch rune
|
||||||
|
for {
|
||||||
|
b.Draw()
|
||||||
|
ch = Getch()
|
||||||
|
|
||||||
|
switch ch {
|
||||||
|
case 'Q':
|
||||||
|
termios.Restore()
|
||||||
|
os.Exit(0)
|
||||||
|
case 'N':
|
||||||
|
b.CreateLane()
|
||||||
|
case 'n':
|
||||||
|
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 'c':
|
||||||
|
// Comment on current story
|
||||||
|
case 'a':
|
||||||
|
// Archive the current story
|
||||||
|
case 'd':
|
||||||
|
// Delete current story
|
||||||
|
case 'D':
|
||||||
|
// Delete current lane
|
||||||
|
case 'e':
|
||||||
|
// Edit current story
|
||||||
|
case ':':
|
||||||
|
b.EnterCommand()
|
||||||
|
case '+':
|
||||||
|
// b.ZoomIn()
|
||||||
|
case '-':
|
||||||
|
// b.ZoomOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Board) ClearMessage() {
|
||||||
|
b.Message = ""
|
||||||
|
b.MsgErr = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Board) SetMessage(msg string, isError bool) {
|
||||||
|
b.Message = msg
|
||||||
|
b.MsgErr = isError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Board) CreateLane() {
|
||||||
|
laneTitle, err := GetAndConfirmCommandLine("Lane Title: ")
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.Lanes = append(b.Lanes, MakeLane(laneTitle))
|
||||||
|
if b.Current < 0 {
|
||||||
|
b.Current = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Board) CreateStory() {
|
||||||
|
b.Lanes[b.Current].CreateStory(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Board) PrintHeader() string {
|
||||||
|
return fmt.Sprintf("%s%s%-*.*s%s\n", style.Header, swimLogo, b.Width-12, b.Width-12, b.Title, styleOff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Board) PrintInputArea() string {
|
||||||
|
return fmt.Sprintf("%s%-*.*s%s\n", style.Input, b.Width, b.Width, " ", styleOff)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Board) PrintLanes() string {
|
||||||
|
var out strings.Builder
|
||||||
|
laneWidth := b.Width / 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])
|
||||||
|
} else {
|
||||||
|
out.WriteString(fmt.Sprintf("%s%*.*s%s", style.Lane, laneWidth, laneWidth, " ", styleOff))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.WriteString(styleOff)
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Board) EnterCommand() {
|
||||||
|
var out strings.Builder
|
||||||
|
out.WriteString(upAndLeft) // Move up one and over all
|
||||||
|
out.WriteString(style.Input)
|
||||||
|
fmt.Print(out.String())
|
||||||
|
command, err := GetLine(":")
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
}
|
||||||
|
b.SetMessage(command, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Board) PrintMessage() string {
|
||||||
|
var out strings.Builder
|
||||||
|
if b.MsgErr {
|
||||||
|
out.WriteString(style.MessageErr)
|
||||||
|
} else {
|
||||||
|
out.WriteString(style.Message)
|
||||||
|
}
|
||||||
|
out.WriteString(fmt.Sprintf("%-*.*s%s", b.Width, b.Width, b.Message, styleOff))
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b Board) Draw() {
|
||||||
|
var out strings.Builder
|
||||||
|
out.WriteString(cursorHome)
|
||||||
|
out.WriteString(b.PrintHeader())
|
||||||
|
out.WriteString(b.PrintLanes())
|
||||||
|
out.WriteString(b.PrintInputArea())
|
||||||
|
out.WriteString(b.PrintMessage())
|
||||||
|
fmt.Print(out.String())
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
const (
|
||||||
|
SimpleColor int = iota
|
||||||
|
EightBitColor
|
||||||
|
TrueColor
|
||||||
|
)
|
||||||
|
|
||||||
|
type Styles struct {
|
||||||
|
Mode int
|
||||||
|
Header string
|
||||||
|
Message string
|
||||||
|
MessageErr string
|
||||||
|
Lane string
|
||||||
|
Input string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var style Styles
|
||||||
|
|
||||||
|
var colors = map[int]map[string]string{
|
||||||
|
SimpleColor: map[string]string{
|
||||||
|
"Header": "\033[34;107;1m", // bold blue on bright white
|
||||||
|
"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
|
||||||
|
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"},
|
||||||
|
TrueColor: map[string]string{
|
||||||
|
"Header": "",
|
||||||
|
"Message": "",
|
||||||
|
"MessageErr": "",
|
||||||
|
"Lane": "",
|
||||||
|
"Input": ""}}
|
||||||
|
|
||||||
|
func (s *Styles) Init(mode int) {
|
||||||
|
if mode == TrueColor || mode == EightBitColor {
|
||||||
|
s.Mode = mode
|
||||||
|
} 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"]
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Comment struct {
|
||||||
|
User string
|
||||||
|
Body string
|
||||||
|
Created time.Time
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Lane struct {
|
||||||
|
Title string
|
||||||
|
Stories []Story
|
||||||
|
Current int // Index of current story
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lane) CreateStory(b *Board) {
|
||||||
|
storyTitle, err := GetAndConfirmCommandLine("Story Title: ")
|
||||||
|
if err != nil {
|
||||||
|
b.SetMessage(err.Error(), true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.Stories = append(l.Stories, MakeStory(storyTitle))
|
||||||
|
if l.Current < 0 {
|
||||||
|
l.Current = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Lane) StringSlice(width int) []string {
|
||||||
|
out := make([]string, 0, len(l.Stories) * 3 + 1)
|
||||||
|
for _, story := range l.Stories {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
if len(out) > 0 {
|
||||||
|
out = append(out, fmt.Sprintf("%s%*.*s%s", style.Lane, width, width, " ", styleOff))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeLane(title string) Lane {
|
||||||
|
return Lane{title, make([]Story,0,5), -1}
|
||||||
|
}
|
88
main.go
88
main.go
|
@ -1,5 +1,89 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
func main() {
|
import (
|
||||||
print("This will be a swim lane project planning application")
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"tildegit.org/sloum/swim/termios"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var board Board
|
||||||
|
|
||||||
|
func Getch() rune {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
char, _, err := reader.ReadRune()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return char
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLine(prefix string) (string, error) {
|
||||||
|
defer termios.SetCharMode()
|
||||||
|
termios.SetLineMode()
|
||||||
|
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
fmt.Print(prefix)
|
||||||
|
fmt.Print("\033[?25h")
|
||||||
|
text, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
fmt.Print("\033[?25l")
|
||||||
|
|
||||||
|
return text[:len(text)-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCommandLine(prefix string) (string, error) {
|
||||||
|
fmt.Print(upAndLeft) // Move up one and over all
|
||||||
|
fmt.Print(style.Input)
|
||||||
|
line, err := GetLine(prefix)
|
||||||
|
fmt.Print(cursorEnd)
|
||||||
|
return line, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAndConfirmCommandLine(prefix string) (string, error) {
|
||||||
|
var conf rune
|
||||||
|
var err error
|
||||||
|
var line string
|
||||||
|
for {
|
||||||
|
line, err = GetCommandLine(prefix)
|
||||||
|
if err != nil {
|
||||||
|
return line, err
|
||||||
|
}
|
||||||
|
VerifyQuery: fmt.Print(upAndLeft) // Move up one and over all
|
||||||
|
fmt.Print(style.Input)
|
||||||
|
fmt.Printf("%s%s%sIs %q correct? (y/n/c)", cursorEnd, upAndLeft, style.Input, line)
|
||||||
|
conf = Getch()
|
||||||
|
if conf == 'y' {
|
||||||
|
break
|
||||||
|
} else if conf == 'n' {
|
||||||
|
continue
|
||||||
|
} else if conf == 'c' {
|
||||||
|
err = fmt.Errorf("Cancelled")
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
goto VerifyQuery
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
style.Init(SimpleColor)
|
||||||
|
cols, rows := termios.GetWindowSize()
|
||||||
|
board = Board{
|
||||||
|
Title: "My Test Board",
|
||||||
|
Body: "Some misc info",
|
||||||
|
Created: time.Now(),
|
||||||
|
Lanes: make([]Lane, 0, 1),
|
||||||
|
Current: -1,
|
||||||
|
Message: "Welcome to SWIM",
|
||||||
|
MsgErr: false,
|
||||||
|
Width: cols,
|
||||||
|
Height: rows,
|
||||||
|
Zoom: 3}
|
||||||
|
|
||||||
|
board.Run()
|
||||||
}
|
}
|
||||||
|
|
9
notes.md
9
notes.md
|
@ -7,22 +7,21 @@ A project planning board for the terminal.
|
||||||
|
|
||||||
1. story
|
1. story
|
||||||
- title string
|
- title string
|
||||||
- details string
|
- body string
|
||||||
- points int
|
- points int
|
||||||
- tag int // enum representing a color
|
- tag int // enum representing a color
|
||||||
- assignee string // username
|
- users []string
|
||||||
- reviewer string // username
|
|
||||||
- comments []comment
|
- comments []comment
|
||||||
- created time.time // the time the story was created
|
- created time.time // the time the story was created
|
||||||
2. comment
|
2. comment
|
||||||
- user string
|
- user string
|
||||||
- comment string
|
- body string
|
||||||
- created time.time
|
- created time.time
|
||||||
3. lane
|
3. lane
|
||||||
- title string
|
- title string
|
||||||
- stories []story
|
- stories []story
|
||||||
4. board
|
4. board
|
||||||
- title string
|
- title string
|
||||||
- details string
|
- body string
|
||||||
- created time.time
|
- created time.time
|
||||||
- lanes []lane
|
- lanes []lane
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Story struct {
|
||||||
|
Title string
|
||||||
|
Body string
|
||||||
|
Users []string
|
||||||
|
Tag int
|
||||||
|
Tasks []Task
|
||||||
|
Comments []Comment
|
||||||
|
Created time.Time
|
||||||
|
Updated time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
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()}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
Body string
|
||||||
|
Complete bool
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package termios
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const (
|
||||||
|
getTermiosIoctl = syscall.TCGETS
|
||||||
|
setTermiosIoctl = syscall.TCSETS
|
||||||
|
)
|
|
@ -0,0 +1,10 @@
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package termios
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const (
|
||||||
|
getTermiosIoctl = syscall.TIOCGETA
|
||||||
|
setTermiosIoctl = syscall.TIOCSETAF
|
||||||
|
)
|
|
@ -0,0 +1,65 @@
|
||||||
|
package termios
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type winsize struct {
|
||||||
|
Row uint16
|
||||||
|
Col uint16
|
||||||
|
Xpixel uint16
|
||||||
|
Ypixel uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
var fd = os.Stdin.Fd()
|
||||||
|
var initial = getTermios()
|
||||||
|
|
||||||
|
func ioctl(fd, request, argp uintptr) error {
|
||||||
|
if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, request, argp); e != 0 {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetWindowSize() (int, int) {
|
||||||
|
var value winsize
|
||||||
|
ioctl(fd, syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&value)))
|
||||||
|
return int(value.Col), int(value.Row)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTermios() syscall.Termios {
|
||||||
|
var value syscall.Termios
|
||||||
|
err := ioctl(fd, getTermiosIoctl, uintptr(unsafe.Pointer(&value)))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTermios(termios syscall.Termios) {
|
||||||
|
err := ioctl(fd, setTermiosIoctl, uintptr(unsafe.Pointer(&termios)))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
runtime.KeepAlive(termios)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetCharMode() {
|
||||||
|
t := getTermios()
|
||||||
|
t.Lflag = t.Lflag ^ syscall.ICANON
|
||||||
|
t.Lflag = t.Lflag ^ syscall.ECHO
|
||||||
|
setTermios(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetLineMode() {
|
||||||
|
var t = getTermios()
|
||||||
|
t.Lflag = t.Lflag | (syscall.ICANON | syscall.ECHO)
|
||||||
|
setTermios(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Restore() {
|
||||||
|
setTermios(initial)
|
||||||
|
}
|
Loading…
Reference in New Issue