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() }