tally/main.go

265 lines
5.4 KiB
Go

package main
import (
"bufio"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"tildegit.org/sloum/tally/termios"
"tildegit.org/sloum/tally/qline"
)
const (
Empty int = iota
Text
Number
Expr
)
var wb workbook
var reAddr *regexp.Regexp = regexp.MustCompile(`^\$?[A-Z]\$?[0-9]+$`)
var reAddrRange *regexp.Regexp = regexp.MustCompile(`^\$?[A-Z]\$?[0-9]+:\$?[A-Z]\$?[0-9]+\+$`)
var reURL *regexp.Regexp = regexp.MustCompile(`^.*://.*$`)
var modified bool = false
type point struct {
row int
col int
}
func runCommand(elems []string) {
if len(elems) == 0 {
return
}
switch elems[0] {
case "write", "w":
if len(elems) >= 2 {
path := ExpandedAbsFilepath(strings.Join(elems[1:], " "))
if filepath.Ext(path) == "" {
path = path + ".tss"
}
WriteFile(path)
wb.path = path
wb.name = filepath.Base(path)
} else {
path := wb.path
ext := filepath.Ext(path)
if ext != ".tss" {
path = path[:len(path)-len(ext)] + ".tss"
}
WriteFile(path)
}
modified = false
case "write-csv", "wc":
if len(elems) >= 2 {
path := ExpandedAbsFilepath(strings.Join(elems[1:], " "))
if filepath.Ext(path) == "" {
path = path + ".csv"
}
WriteCsv(path, false)
} else {
path := wb.path
ext := filepath.Ext(path)
if ext != ".csv" {
path = path[:len(path)-len(ext)] + ".csv"
}
WriteCsv(path, false)
}
case "write-tsv", "wt":
if len(elems) >= 2 {
path := ExpandedAbsFilepath(strings.Join(elems[1:], " "))
if filepath.Ext(path) == "" {
path = path + ".tsv"
}
WriteCsv(path, true)
} else {
path := wb.path
ext := filepath.Ext(path)
if ext != ".tsv" {
path = path[:len(path)-len(ext)] + ".tsv"
}
WriteCsv(path, true)
}
case "trim", "t":
wb.sheets[wb.sheet].TrimSheet()
fmt.Print("\033[2J") // Clear the screen
case "recalculate", "r":
wb.sheets[wb.sheet].Recalculate()
}
}
func Getch() (rune, error) {
reader := bufio.NewReader(os.Stdin)
return qline.ReadKey(reader)
}
func GetEditLine(prefix, content string, width int) string {
fmt.Print("\033[?25h")
s := qline.GetInput(prefix, content, width)
fmt.Print("\033[?25h")
return s
}
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 DigitCount(num int) int {
counter := 0
for ; num > 0; num /= 10 {
counter++
}
return counter
}
func GetCell(p point) (cell, error) {
if p.row > wb.sheets[wb.sheet].rows || p.col > wb.sheets[wb.sheet].cols {
return cell{}, fmt.Errorf("Invalid reference")
}
return wb.sheets[wb.sheet].cells[p.row][p.col], nil
}
func Addr2Point(addr string) (point, bool, bool) {
lockRow, lockCol := false, false
p := point{}
if strings.HasPrefix(addr, "$") {
lockCol = true
addr = addr[1:]
}
if len(addr) > 2 && addr[1] == '$' {
lockRow = true
addr = fmt.Sprintf("%c%s", addr[0], addr[2:])
}
p.col = int(addr[0]) - 65
p.row, _ = strconv.Atoi(addr[1:])
p.row--
return p, lockRow, lockCol
}
func Point2Addr(p point, lockRow, lockCol bool) string {
var s strings.Builder
if lockCol {
s.WriteRune('$')
}
s.WriteRune(rune(p.col) + 65)
if lockRow {
s.WriteRune('$')
}
s.WriteString(strconv.Itoa(p.row+1))
return s.String()
}
func ApplyPointDiff(p1, p2 point) point {
return point{row: p2.row - p1.row, col: p2.col - p1.col}
}
func IsAddr(addr string) bool {
return reAddr.MatchString(addr)
}
func IsRange(addr string) bool {
return reAddrRange.MatchString(addr)
}
func IsURL(u string) bool {
return reURL.MatchString(u)
}
func IsFunc(val string) bool {
switch strings.ToUpper(val) {
case "+", "-", "/", "*", "MIN", "MAX",
"ROUND", "SUM", "SPC", ".", "SWAP",
"DUP", "DROP", "CLEAR", "OVER", "POW",
"FLOOR", "CEIL":
return true
default:
return 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\033[2J")
wb.Draw()
case syscall.SIGINT:
termios.Restore()
fmt.Print("\033[?25h")
os.Exit(1)
}
}
}
func printHelp() {
header := `tally - spreadsheets
Syntax: tally [filepath]
Examples: tally
tally -h
tally ./my-tally-spreadsheet.tss
tally ./my-cool-csv.csv
tally ./my-cool-tsv.tsv
filepath should be a path to a file that already exists. To create a new file, simply run tally with no arguments and then save the file however you like.
`
_, _ = fmt.Fprintf(os.Stdout, header)
flag.PrintDefaults()
}
func main() {
flag.Usage = printHelp
flag.Parse()
args := flag.Args()
if len(args) > 0 {
path := ExpandedAbsFilepath(args[0])
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".tss":
LoadFile(path)
case ".csv":
LoadCsv(path, false)
case ".tsv":
LoadCsv(path, true)
default:
fmt.Fprintf(os.Stderr, "Unknown file type %q", ext)
os.Exit(1)
}
wb.sheets[wb.sheet].Recalculate()
} else {
wb = makeWorkbook("blank.tss")
}
// 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)
wb.Run()
}