Initial commit of mostly working spreadsheet program

This commit is contained in:
sloum 2021-03-14 19:45:56 -07:00
commit db0c8a1403
9 changed files with 741 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
tally
*.tss

191
cell.go Normal file
View File

@ -0,0 +1,191 @@
package main
import (
"fmt"
"strconv"
"strings"
)
type cell struct {
kind int // Enum for the type of cell
rawVal string // A raw string of the value
num float64 // A quick reference field for the current value
expr []string // Fields representing an expression
mask string // What the user sees
mods []int // Enums of modifiers on the cell
}
func (c cell) String(width int, selected bool) string {
mods := ""
if selected {
mods += "\033[7m"
}
return fmt.Sprintf("%s%*.*s\033[0m", mods, width, width, c.mask)
}
func (c *cell) Edit(row, col int) {
line, err := GetLine(fmt.Sprintf("\033[2KUpdate %c%d: \033[?25h", col+64, row))
if err != nil {
panic(err)
}
fmt.Print("\033[?25l")
line = strings.TrimSpace(line)
c.Update(line)
}
func (c *cell) Update(val string) bool {
if val == c.rawVal || len(val) == 0 {
// If nothing was changed or the change is empty just return
return false
}
c.rawVal = val
num, err := strconv.ParseFloat(val, 64)
if err == nil {
c.kind = Number
c.num = num
c.mask = strconv.FormatFloat(num, 'f', -1, 64)
} else if strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"") {
c.kind = Text
c.mask = strings.Replace(val[1:len(val)-1], "\\_", " ", -1)
} else {
c.kind = Expr
c.expr = strings.Fields(val)
}
return true
}
func (c *cell) Calculate() {
if c.kind != Expr {
return
}
if len(c.expr) < 2 {
c.mask = "#Err"
return
}
insideString := false
s := makeStack()
for _, v := range c.expr {
if IsAddr(v) {
c, err := GetCell(Addr2Point(v))
if err != nil {
c.mask = "#Err"
return
}
if c.kind == Text {
v = c.rawVal
} else {
v = c.mask
}
} else if IsRange(v) {
// TODO Apply range
}
if s.ptr >= 0 && s.text {
c.mask = "#Err"
return
}
switch true {
case strings.HasPrefix(v, "\""):
if s.ptr >= 0 {
c.mask = "#Err"
return
}
if !strings.HasSuffix(v, "\"") {
insideString = true
}
s.str.WriteString(strings.Replace(v, "\"", "", -1))
s.text = true
case insideString:
if s.ptr >= 0 {
c.mask = "#Err"
return
}
if strings.HasSuffix(v, "\"") {
insideString = false
v = strings.Replace(v, "\"", "", -1)
}
s.str.WriteRune(' ')
s.str.WriteString(v)
case IsFunc(v):
if s.text {
c.mask = "#Err"
return
} else if v == "+" {
s.Add()
} else if v == "-" {
s.Subtract()
} else if v == "/" {
s.Divide()
} else if v == "*" {
s.Multiply()
}
default:
v, err := strconv.ParseFloat(v, 64)
if err != nil {
c.mask = "#Err"
return
}
s.Push(v)
}
}
if s.text {
c.mask = strings.Replace(s.str.String(), "\\_", " ", -1)
} else if s.ptr == 0 {
c.mask = strconv.FormatFloat(s.data[0], 'f', -1, 64)
} else {
c.mask = "Err"
}
}
type stack struct {
data [100]float64
ptr int
text bool
str strings.Builder
}
func makeStack() stack {
return stack{data: [100]float64{}, ptr: -1, text: false}
}
func (s *stack) Push(v float64) {
if s.ptr >= 99 {
panic("Stack overflow")
}
s.ptr++
s.data[s.ptr] = v
}
func (s *stack) Pop() float64 {
if s.ptr < 0 {
panic("Stack underflow")
}
s.ptr--
return s.data[s.ptr+1]
}
func (s *stack) Add() {
if s.text {
val := strconv.FormatFloat(s.Pop(), 'f', -1, 64)
s.str.WriteString(val)
} else {
s.Push(s.Pop() + s.Pop())
}
}
func (s *stack) Subtract() {
second := s.Pop()
s.Push(s.Pop() - second)
}
func (s *stack) Multiply() {
s.Push(s.Pop() * s.Pop())
}
func (s *stack) Divide() {
second := s.Pop()
s.Push(s.Pop() / second)
}

114
main.go Normal file
View File

@ -0,0 +1,114 @@
package main
import (
"bufio"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"tildegit.org/sloum/spreadsheet/termios"
)
const (
Empty int = iota
Text
Number
Expr
Bold
Italic
)
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]+$`)
type point struct {
row int
col int
}
func runCommand(elems []string) {
}
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)
text, err := reader.ReadString('\n')
if err != nil {
return "", err
}
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 {
p := point{}
p.col = int(addr[0]) - 65
p.row, _ = strconv.Atoi(addr[1:])
p.row--
return p
}
func Point2Addr(p point) string {
var s strings.Builder
s.WriteRune(rune(p.col) + 65)
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 IsFunc(val string) bool {
switch strings.ToUpper(val) {
case "+", "-", "/", "*", "^", "MIN", "MAX", "SQRT",
"ROUND", "SUBSTR", "UPPER", "LOWER", "SUM":
return true
default:
return false
}
}
func main() {
wb = makeWorkbook("test.qsh")
wb.Run()
}

19
notes.md Normal file
View File

@ -0,0 +1,19 @@
- Sheets can have any number of rows
- Sheets have a max number of cols (26)
- Text and Float are the only types
- Expressions are forth style: A2 B3 + C2 A2 - +
- Ranges work via ':', for example: A2:C6 +
- Available expression functions: +, - . \*, /, POW, MIN, MAX, SQRT, ROUND, SUBSTR, UPPER, LOWER, CAPITAL, SUM
TODO:
- Write expression parser
- Add methods for running expressions
- Figure out relative reference logic (when you copy/paste an expression)
- File save/save-as, custom format?
- File load: custom format, csv
- Ability to add/remove/rename sheets
- Improve TUI
- Make manpage
- Make Makefile

221
sheet.go Normal file
View File

@ -0,0 +1,221 @@
package main
import (
"fmt"
"strings"
)
type sheet struct {
name string
selection point
cells [][]cell
cols int
rows int
zoom int
rowOff int
colOff int
yankBuff cell
yankPoint point
}
func (s sheet) Draw(termCols, termRows int) {
var selected bool
rowDigits := DigitCount(len(s.cells))
width := (termCols - rowDigits) / s.zoom
if width * s.zoom + rowDigits + 1 > termCols {
width -= 1
}
// Print column header
fmt.Printf("%s ", strings.Repeat(" ", rowDigits))
for i := s.colOff+1; i <= s.colOff + s.zoom; i++ {
if i > s.cols {
break
}
pre := "\033[7m"
if i == s.selection.col {
pre = ""
}
fmt.Printf("%s%-*c\033[0m", pre, width, i+64)
}
fmt.Print("\n")
// Print Rows
for row, r := range s.cells[s.rowOff:] {
if row >= termRows-5 {
break
}
mod := "\033[7m"
if row + s.rowOff == s.selection.row-1 {
mod = ""
}
fmt.Printf("%s%-*d \033[0m", mod, rowDigits, row+1+s.rowOff)
// Print Cells
for col, cell := range r[s.colOff:] {
if col >= s.zoom {
break
}
selected = false
if s.selection.row-1 == row + s.rowOff && s.selection.col-1 == col + s.colOff {
selected = true
}
fmt.Printf(cell.String(width, selected))
}
fmt.Print("\n\033[2K")
}
}
func (s sheet) CurrentValue(raw bool) string {
if raw {
return s.cells[s.selection.row-1][s.selection.col-1].rawVal
}
return s.cells[s.selection.row-1][s.selection.col-1].mask
}
func (s *sheet) Recalculate() {
for row, _ := range s.cells {
for col, _ := range s.cells[row] {
// TODO do this concurrently
s.cells[row][col].Calculate()
}
}
}
func (s *sheet) AddRows(count int) {
for ;count > 0; count-- {
s.cells = append(s.cells, make([]cell, s.cols))
s.rows++
}
}
func (s *sheet) AddCols(count int) {
for ;count > 0; count-- {
for i, _ := range s.cells {
s.cells[i] = append(s.cells[i], cell{})
}
s.cols++
}
}
func (s *sheet) Yank() {
c := s.cells[s.selection.row-1][s.selection.col-1]
s.yankBuff = cell{}
s.yankBuff.kind = c.kind
s.yankBuff.rawVal = c.rawVal
s.yankBuff.num = c.num
s.yankBuff.mask = c.mask
s.yankBuff.expr = append(s.yankBuff.expr, c.expr...)
s.yankBuff.mods = append(s.yankBuff.mods, c.mods...)
s.yankPoint = point{row: s.selection.row-1, col: s.selection.col-1}
}
func (s *sheet) Paste() {
c := cell{}
c.kind = s.yankBuff.kind
c.rawVal = s.yankBuff.rawVal
c.num = s.yankBuff.num
c.mask = s.yankBuff.mask
c.expr = append(c.expr, s.yankBuff.expr...)
c.mods = append(c.mods, s.yankBuff.mods...)
s.cells[s.selection.row-1][s.selection.col-1] = c
}
func (s *sheet) PasteRelative() {
c := cell{}
c.kind = s.yankBuff.kind
c.rawVal = s.yankBuff.rawVal
c.num = s.yankBuff.num
c.mask = s.yankBuff.mask
c.expr = append(c.expr, s.yankBuff.expr...)
c.mods = append(c.mods, s.yankBuff.mods...)
if c.kind == Expr {
for i, v := range c.expr {
if IsAddr(v) {
refPoint := Addr2Point(v)
diff := point{row: s.yankPoint.row-s.selection.row+1, col: s.yankPoint.col-s.selection.col+1}
diffApply := point{row: refPoint.row-diff.row, col: refPoint.col-diff.col}
p2a := Point2Addr(diffApply)
c.expr[i] = p2a
}
}
c.rawVal = strings.Join(c.expr, " ")
}
s.cells[s.selection.row-1][s.selection.col-1] = c
}
func (s *sheet) moveSelection(dir rune, termRows int) {
switch dir {
case Left:
if s.selection.col > 1 {
s.selection.col--
}
if s.selection.col <= s.colOff {
s.colOff--
}
case Right:
s.selection.col++
if s.selection.col > s.cols && s.selection.col < 27 {
s.AddCols(1)
}
if s.selection.col > s.colOff + s.zoom && s.cols > s.zoom {
s.colOff++
}
if s.selection.col > 26 {
s.selection.col = 26
}
case Up:
if s.selection.row > 1 {
s.selection.row--
}
if s.selection.row <= s.rowOff {
s.rowOff--
}
case Down:
s.selection.row++
if s.selection.row > s.rows {
s.AddRows(1)
}
if s.selection.row > s.rowOff + termRows - 5 && s.rows > termRows - 5 {
s.rowOff++
}
case RowStart:
s.selection.col = 1
s.colOff = 0
case ToTop:
s.selection.row = 1
s.rowOff = 0
case RowEnd:
s.selection.col = s.cols
s.colOff = s.cols - s.zoom
if s.colOff < 0 {
s.colOff = 0
}
case ToBottom:
s.selection.row = s.rows
s.rowOff = s.rows - termRows + 5
if s.rowOff < 0 {
s.rowOff = 0
}
}
}
func (s *sheet) ZoomIn() {
if s.zoom == 1 {
return
}
s.zoom-- // Zooming in reduces number of cols viewed
}
func (s *sheet) ZoomOut() {
s.zoom++ // Zooming out increases number of cols viewed
}
func makeSheet(name string) sheet {
if name == "" {
name = "Sheet1"
}
row := make([][]cell,1, 26)
row[0] = make([]cell, 1, 26)
return sheet{name, point{1,1}, row, 1, 1, 6, 0, 0, cell{}, point{}}
}

10
termios/consts_linux.go Normal file
View File

@ -0,0 +1,10 @@
// +build linux
package termios
import "syscall"
const (
getTermiosIoctl = syscall.TCGETS
setTermiosIoctl = syscall.TCSETS
)

View File

@ -0,0 +1,10 @@
// +build !linux
package termios
import "syscall"
const (
getTermiosIoctl = syscall.TIOCGETA
setTermiosIoctl = syscall.TIOCSETAF
)

65
termios/termios.go Normal file
View File

@ -0,0 +1,65 @@
package termios
import (
"os"
"runtime"
"syscall"
"unsafe"
)
type winsize struct {
Row uint16
Col uint16
Xpixel uint16
Ypixel uint16
}
var fd = os.Stdin.Fd()
var initial = getTermios()
func ioctl(fd, request, argp uintptr) error {
if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, request, argp); e != 0 {
return e
}
return nil
}
func GetWindowSize() (int, int) {
var value winsize
ioctl(fd, syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&value)))
return int(value.Col), int(value.Row)
}
func getTermios() syscall.Termios {
var value syscall.Termios
err := ioctl(fd, getTermiosIoctl, uintptr(unsafe.Pointer(&value)))
if err != nil {
panic(err)
}
return value
}
func setTermios(termios syscall.Termios) {
err := ioctl(fd, setTermiosIoctl, uintptr(unsafe.Pointer(&termios)))
if err != nil {
panic(err)
}
runtime.KeepAlive(termios)
}
func SetCharMode() {
t := getTermios()
t.Lflag = t.Lflag ^ syscall.ICANON
t.Lflag = t.Lflag ^ syscall.ECHO
setTermios(t)
}
func SetLineMode() {
var t = getTermios()
t.Lflag = t.Lflag | (syscall.ICANON | syscall.ECHO)
setTermios(t)
}
func Restore() {
setTermios(initial)
}

109
workbook.go Normal file
View File

@ -0,0 +1,109 @@
package main
import (
"fmt"
"os"
"strings"
"tildegit.org/sloum/spreadsheet/termios"
)
const (
Left rune = 'h'
Right rune = 'l'
Up rune = 'k'
Down rune = 'j'
Quit rune = 'Q'
Edit rune = '\n'
EditAlt rune = ' '
Del rune = 'd'
Com rune = ':'
ZoomIn rune = '+'
ZoomOut rune = '-'
Yank rune = 'y'
Paste rune = 'p'
PasteRelative rune = 'P'
ToTop rune = 'g'
ToBottom rune = 'G'
RowStart rune = '^'
RowEnd rune = '$'
)
type workbook struct {
name string
path string
sheets []sheet
sheet int
termRows int
termCols int
}
func (w workbook) Draw() {
fmt.Print("\033[0;0H")
fmt.Printf("FILE: \033[1m%-*.*s\033[0m\n", w.termCols-6, w.termCols-6, w.path)
fmt.Printf("EXPR: %-*.*s\n",
w.termCols-6, w.termCols-6,
w.sheets[w.sheet].CurrentValue(true))
w.sheets[w.sheet].Draw(w.termCols,w.termRows)
}
func (w *workbook) Run() {
defer termios.Restore()
termios.SetCharMode()
fmt.Print("\033[?25l\033[2J")
var input rune
for {
w.Draw()
input = Getch()
switch input {
case Left, Right, Up, Down, ToTop, RowStart, ToBottom, RowEnd:
w.sheets[w.sheet].moveSelection(input, w.termRows)
case Quit:
termios.Restore()
fmt.Print("\033[?25h")
os.Exit(0)
case Edit, EditAlt:
w.sheets[w.sheet].cells[w.sheets[w.sheet].selection.row-1][w.sheets[w.sheet].selection.col-1].Edit(w.sheets[w.sheet].selection.row, w.sheets[w.sheet].selection.col)
w.sheets[w.sheet].Recalculate()
case Del:
w.sheets[w.sheet].cells[w.sheets[w.sheet].selection.row-1][w.sheets[w.sheet].selection.col-1] = cell{}
w.sheets[w.sheet].Recalculate()
case Com:
line, err := GetLine(":")
if err != nil {
panic(err)
}
runCommand(strings.Fields(line))
case ZoomIn:
w.sheets[w.sheet].ZoomIn()
fmt.Print("\033[2J")
case ZoomOut:
w.sheets[w.sheet].ZoomOut()
fmt.Print("\033[2J")
case Yank:
w.sheets[w.sheet].Yank()
case Paste:
w.sheets[w.sheet].Paste()
w.sheets[w.sheet].Recalculate()
case PasteRelative:
w.sheets[w.sheet].PasteRelative()
w.sheets[w.sheet].Recalculate()
}
}
}
func makeWorkbook(path string) workbook {
// TODO parse path and get file name
// set name of strict to the filename
// or untitled.qsh if no filename; set
// path to the full path
cols, rows := termios.GetWindowSize()
sh := makeSheet("Sheet1")
wb = workbook{path, path, make([]sheet,1), 0, rows, cols}
wb.sheets[0] = sh
return wb
}
func (w workbook) header() string {
return fmt.Sprintf("\033[1;7m %s \033[0m %s\n",w.name, w.sheets[w.sheet].name)
}