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
|
||||
|
||||
func main() {
|
||||
print("This will be a swim lane project planning application")
|
||||
import (
|
||||
"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
|
||||
- title string
|
||||
- details string
|
||||
- body string
|
||||
- points int
|
||||
- tag int // enum representing a color
|
||||
- assignee string // username
|
||||
- reviewer string // username
|
||||
- users []string
|
||||
- comments []comment
|
||||
- created time.time // the time the story was created
|
||||
2. comment
|
||||
- user string
|
||||
- comment string
|
||||
- body string
|
||||
- created time.time
|
||||
3. lane
|
||||
- title string
|
||||
- stories []story
|
||||
4. board
|
||||
- title string
|
||||
- details string
|
||||
- body string
|
||||
- created time.time
|
||||
- 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