Initial commit of mostly working spreadsheet program
This commit is contained in:
commit
db0c8a1403
|
@ -0,0 +1,2 @@
|
||||||
|
tally
|
||||||
|
*.tss
|
|
@ -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)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
|
@ -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{}}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package termios
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const (
|
||||||
|
getTermiosIoctl = syscall.TCGETS
|
||||||
|
setTermiosIoctl = syscall.TCSETS
|
||||||
|
)
|
|
@ -0,0 +1,10 @@
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package termios
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
const (
|
||||||
|
getTermiosIoctl = syscall.TIOCGETA
|
||||||
|
setTermiosIoctl = syscall.TIOCSETAF
|
||||||
|
)
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue