259 lines
5.5 KiB
Go
259 lines
5.5 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"
|
|
)
|
|
|
|
var board Board
|
|
var fp string
|
|
|
|
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 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":
|
|
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() {
|
|
termios.Restore()
|
|
fmt.Print(cursorEnd)
|
|
fmt.Print("\033[0m\n\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 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 "8":
|
|
return SimpleColor
|
|
case "256":
|
|
return EightBitColor
|
|
case "TRUE":
|
|
return TrueColor
|
|
case "NONE", "OFF":
|
|
return Term
|
|
default:
|
|
return -1
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
colors := flag.String("color", "", "Color mode: 8, 256, True, None" )
|
|
flag.Parse()
|
|
args := flag.Args()
|
|
style.Init(SetColorFromFlag(*colors))
|
|
cols, rows := termios.GetWindowSize()
|
|
if len(args) > 0 {
|
|
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()
|
|
}
|