265 lines
5.4 KiB
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()
|
|
}
|