2
1
Fork 0
swim/main.go

322 lines
6.9 KiB
Go

package main
// swim is a project management board for the terminal of unix or
// unix-like systems.
//
// Copyright (C) 2021 Brian Evans, All Rights Reserved
//
// This program is free, as in beer, software: you can redistribute it
// and/or modify it under the terms of the Floodgap Free Software license.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// Floodgap Free Software License for more details.
//
// You should have received a copy of the Floodgap Free Software License
// along with this program. If not, see:
// 1. http://www.floodgap.com/software/ffsl/
// 2. gopher://gopher.floodgap.com/1/ffsl/
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"os/signal"
"os/user"
"path/filepath"
"strings"
"syscall"
"tildegit.org/sloum/swim/termios"
"tildegit.org/sloum/swim/qline"
)
var board Board
var fp string
var unsavedChanges bool
func ExpandedAbsFilepath(p string) string {
if strings.HasPrefix(p, "~/") {
usr, _ := user.Current()
homedir := usr.HomeDir
p = filepath.Join(homedir, p[2:])
}
path, _ := filepath.Abs(p)
return path
}
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 GetEditableLine(prompt, defaultText string) string {
fmt.Printf("%s%s\033[2K\033[?25h", upAndLeft, style.Input)
str := qline.GetInput(prompt, defaultText, board.width)
fmt.Print("\033[?25l")
fmt.Print(cursorEnd)
return str
}
func GetCommandLine(prefix string) (string, error) {
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", "ya", "yaas", "aye":
return true, nil
default:
return false, nil
}
}
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
}
if line == "" {
return line, fmt.Errorf("Cancelled input")
}
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' || conf == '\n' {
break
} else if conf == 'n' {
fmt.Print(cursorEnd)
continue
} else if conf == 'c' {
err = fmt.Errorf("Cancelled input")
break
} else {
fmt.Print(cursorEnd)
goto VerifyQuery
}
}
return line, err
}
// Adapted From:
// https://gist.github.com/kennwhite/306317d81ab4a885a965e25aa835b8ef
func WrapText(text string, lineWidth int) string {
var wrapped strings.Builder
words := strings.Fields(strings.TrimSpace(text))
if len(words) == 0 {
return text
}
wrapped.WriteString(words[0])
spaceLeft := lineWidth - wrapped.Len()
for _, word := range words[1:] {
if len(word)+1 > spaceLeft {
wrapped.WriteRune('\n')
wrapped.WriteString(word)
spaceLeft = lineWidth - len(word)
} else {
wrapped.WriteRune(' ')
wrapped.WriteString(word)
spaceLeft -= 1 + len(word)
}
}
return wrapped.String()
}
func Quit() {
if unsavedChanges {
quitAnyway, _ := GetConfirmation("There are unsaved changed. Quit anyway? (y, n) ")
if !quitAnyway {
board.SetMessage("Quitting with unsaved work cancelled", false)
return
}
}
termios.Restore()
fmt.Print(cursorEnd)
fmt.Print("\033[0m\033[?25h")
os.Exit(0)
}
func LoadFile(path string, cols, rows int) {
fp = ExpandedAbsFilepath(path)
bytes, err := ioutil.ReadFile(fp)
if err != nil {
board = DefaultBoard(cols, rows)
board.SetMessage("Could not load file at path, new file created", false)
return
}
err = json.Unmarshal(bytes, &board)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not understand input file:\n%s\n", err.Error())
os.Exit(1)
}
board.width = cols
board.height = rows
board.laneOff = 0
board.Current = -1
board.StoryOpen = false
if len(board.Lanes) > 0 {
board.Current = 0
}
for i := range board.Lanes {
board.Lanes[i].ResetLoadPositions()
for s := range board.Lanes[i].Stories {
board.Lanes[i].Stories[s].offset = 0
}
}
board.SetMessage(fmt.Sprintf("Loaded file: %s", path), false)
}
func handleSignals(c <-chan os.Signal) {
for {
switch <-c {
case syscall.SIGTSTP:
termios.Restore()
fmt.Print("\033[?25h")
_ = syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
case syscall.SIGCONT:
termios.SetCharMode()
fmt.Print("\033[?25l")
board.Draw()
case syscall.SIGINT:
termios.Restore()
fmt.Print("\033[?25h")
os.Exit(1)
}
}
}
func InitNewBoard() {
b := DefaultBoard(80,30)
b.CreateLane("Backlog")
b.CreateLane("Active")
b.CreateLane("Complete")
b.message = "Welcome to SWIM"
workingPath, err := os.Getwd()
if err != nil {
panic(err.Error())
}
filename := filepath.Base(workingPath) + ".swim"
filename = strings.Replace(filename, " ", "_", -1)
savepath := filepath.Join(workingPath, filename)
b.Write([]string{"write", savepath})
fmt.Printf("Swim file initialized: %s", savepath)
Quit()
}
func DefaultBoard(cols, rows int) Board {
return Board{
Title: "",
Lanes: make([]Lane, 0, 1),
Current: -1,
laneOff: 0,
message: "Welcome to SWIM",
msgErr: false,
width: cols,
height: rows,
Zoom: 3}
}
func SetColorFromFlag(f string) int {
f = strings.ToUpper(f)
switch f {
case "SIMPLE":
return SimpleColor
case "256":
return EightBitColor
case "TRUE":
return TrueColor
case "NONE", "OFF":
return Term
default:
return -1
}
}
func printHelp() {
header := `swim - project board
Syntax: swim [flags] [option or filepath]
Examples:
swim
swim -h
swim init
swim ./my-board.swim
swim -color true ./my-board.swim
Options:
init
initialize a board with three default lanes
Flags:
`
_, _ = fmt.Fprintf(os.Stdout, header)
flag.PrintDefaults()
}
func main() {
flag.Usage = printHelp
colors := flag.String("color", "", "Color mode: Simple, 256, True, None" )
flag.Parse()
args := flag.Args()
style.Init(SetColorFromFlag(*colors))
cols, rows := termios.GetWindowSize()
if len(args) > 0 {
if strings.ToLower(args[0]) == "init" {
InitNewBoard()
} else {
LoadFile(args[0], cols, rows)
}
} else {
fp = ""
board = DefaultBoard(cols, rows)
}
// watch for signals, send them to be handled
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTSTP, syscall.SIGCONT, syscall.SIGINT)
go handleSignals(c)
board.Run()
}