commit 8b537d30a469b8e15d6762726a0701c93edcd41f Author: sloum Date: Sun May 23 22:21:25 2021 -0700 Initial commit diff --git a/main.go b/main.go new file mode 100644 index 0000000..644eab7 --- /dev/null +++ b/main.go @@ -0,0 +1,357 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "math" + "os" + "strconv" + "strings" + + "tildegit.org/sloum/nums/qline" + "tildegit.org/sloum/nums/termios" +) + +const ( + add int = iota + sub + mul + div + pow + rnd + flr + cil + abs + inv + sqt +) + +type stack struct { + ptr int + data [100]float64 +} + +func (s *stack) Push(v float64) { + if s.ptr >= 99 { + PrintError("Stack overflow") + s.Clear() + return + } + s.ptr++ + s.data[s.ptr] = v +} + +func (s *stack) Pop() (float64, error) { + if s.ptr < 0 { + PrintError("Stack underflow") + s.Clear() + return 0.0, fmt.Errorf("Stack underflow") + } + s.ptr-- + return s.data[s.ptr+1], nil +} + +func (s *stack) Clear() { + s.ptr = -1 +} + +func (s stack) Len() int { + return s.ptr+1 +} + +func (s *stack) Add() { + n2, err := s.Pop() + if err != nil { + return + } + n1, err := s.Pop() + if err != nil { + return + } + s.Push(n1+n2) +} + +func (s *stack) Sub() { + n2, err := s.Pop() + if err != nil { + return + } + n1, err := s.Pop() + if err != nil { + return + } + s.Push(n1-n2) +} + +func (s *stack) Mul() { + n2, err := s.Pop() + if err != nil { + return + } + n1, err := s.Pop() + if err != nil { + return + } + s.Push(n1*n2) +} + +func (s *stack) Div() { + n2, err := s.Pop() + if err != nil { + return + } + n1, err := s.Pop() + if err != nil { + return + } + s.Push(n1/n2) +} + +func (s *stack) Pow() { + exp, err := s.Pop() + if err != nil { + return + } + base, err := s.Pop() + if err != nil { + return + } + s.Push(math.Pow(base, exp)) +} + + +func (s *stack) Floor() { + n, err := s.Pop() + if err != nil { + return + } + s.Push(math.Floor(n)) +} + +func (s *stack) Ceil() { + n, err := s.Pop() + if err != nil { + return + } + s.Push(math.Ceil(n)) +} + +func (s *stack) Round() { + decimals, err := s.Pop() + if err != nil { + return + } + num, err := s.Pop() + if err != nil { + return + } + s.Push(math.Round(num*math.Pow(10, decimals)) / math.Pow(10, decimals)) +} + +func (s *stack) Abs() { + n, err := s.Pop() + if err != nil { + return + } + if n < 0 { + s.Push(n*-1) + } else { + s.Push(n) + } +} + +func (s *stack) Inv() { + n, err := s.Pop() + if err != nil { + return + } + s.Push(n*-1) +} + +func (s *stack) Sqt() { + n, err := s.Pop() + if err != nil { + return + } + + s.Push(math.Sqrt(n)) +} + +func (s *stack) Dup() { + val, err := s.Pop() + if err != nil { + return + } + s.Push(val) + s.Push(val) +} + +func (s *stack) Ovr() { + tos, err := s.Pop() + if err != nil { + return + } + cpy, err := s.Pop() + if err != nil { + return + } + s.Push(cpy) + s.Push(tos) + s.Push(cpy) +} + +func (s *stack) Swp() { + tos, err := s.Pop() + if err != nil { + return + } + newTos, err := s.Pop() + if err != nil { + return + } + s.Push(tos) + s.Push(newTos) +} + +func (s *stack) Drp() { + s.Pop() +} + +func (s stack) TOS() string { + if s.ptr < 0 { + return "0" + } + return strconv.FormatFloat(s.data[s.ptr], 'f', -1, 64) +} + +func PrintError(s string) { + fmt.Fprintf(os.Stderr, "\033[1;91m[Err] %s,\033[0m\n", s) +} + +func PrintOperatorHelp() { + txt := `# General + +This calculator uses reverse polish notation. As such, the operators come after the operands. For example: + + 5 7 + 1 - + +The output of the above would be '11' (using infix notation this would be written ' 5 + 7 - 1'). For more information on reverse polish notation see: https://en.wikipedia.org/wiki/Reverse_Polish_notation + +The output of a calculation will always be the top of the stack. If there is more than one value on the stack at the end of a calculation a warning will be output to stderr. + +# Command List + +The following commands all have a three character version and a single character version. Either one is valid. They are displayed as a comma separated values (single character version first) followed by a colon and a description of the operator. The operators are case insensitive. + + +, add: Adds the top two values on the stack + -, sub: Subtracts the top stack value from the value underneath it + /, div: Divides the top two stack values with the top of stack being the divisor and the value below it on the stack being the dividend + *, mul: Multiplies the top two values on the stack + ^, pow: Raises the value underneath top of stack to the power of top of stack + @, rnd: Rounds the value underneath top of stack to the number of decimal places represented by top of stack. If top of stack is not a whole number an error will result + >, cil: Performs a ceiling function on the top of stack (rounds it up to the next whole number) + <, flr: Performs a floor function on the top of stack (rounds it down to the next whole number) + |, abs: Takes the top value from the stack and leaves its absolute value + !, inv: Multiplies top of stack by -1 + V, sqt: Takes the top value from the stack and leaves its square root + +The following commands operate on the stack, rather than as numerical operations: + + x, swp: Swap the position of the top two values on the stack + #, dup: Duplicate/copy the value on top of the stack such that the copy is now above the original on the stack + _, clr: Clear the stack (wipe out all values and start over) + ., drp: Drop the top value on the stack (remove it and throw it away) + + ?, help: Prints this help message +` + fmt.Fprintf(os.Stderr, "%s\033\n", txt) +} + +func processWord(w string, s *stack) { + num, err := strconv.ParseFloat(w, 64) + if err == nil { + s.Push(num) + return + } + w = strings.ToLower(w) + switch w { + case "+", "add": + s.Add() + case "-", "sub": + s.Sub() + case "*", "mul": + s.Mul() + case "/", "div": + s.Div() + case "^", "pow": + s.Pow() + case "@", "rnd": + s.Round() + case ">", "cil": + s.Ceil() + case "<", "flr": + s.Floor() + case "|", "abs": + s.Abs() + case "!", "inv": + s.Inv() + case "V", "sqt": + s.Sqt() + case "#", "dup": + s.Dup() + case "x", "swp": + s.Swp() + case "_", "clr": + s.Clear() + case ".", "drp": + s.Drp() + case "?", "help": + PrintOperatorHelp() + s.Clear() + default: + PrintError(fmt.Sprintf("Unknown operator: %q", w)) + s.Clear() + } +} + +func main() { + pipe := flag.Bool("p", false, "Take input from a pipe") + flag.Parse() + s := stack{-1, [100]float64{}} + + if !*pipe { + termios.SetInitialTermios() + defer termios.Restore() + termios.SetCharMode() + + for { + var cols, _ = termios.GetWindowSize() + ln := qline.GetInput("", "", cols) + f := strings.Fields(ln) + if len(f) == 0 { + break + } + fmt.Print("\n") + for _, val := range f { + processWord(val, &s) + } + fmt.Fprintf(os.Stderr, "%v\n", s.data[:s.ptr+1]) + } + } else { + b, err := ioutil.ReadAll(os.Stdin) + if err != nil { + fmt.Fprint(os.Stderr, "Unable to read from pipe") + } + f := strings.Fields(string(b)) + for _, val := range f { + processWord(val, &s) + } + if s.ptr >= 0 { + fmt.Fprintf(os.Stderr, "%v\n", s.data[s.ptr]) + } else { + os.Exit(1) + } + } + +} diff --git a/nums b/nums new file mode 100755 index 0000000..5cfcf08 Binary files /dev/null and b/nums differ diff --git a/qline/qline.go b/qline/qline.go new file mode 100644 index 0000000..9c79c77 --- /dev/null +++ b/qline/qline.go @@ -0,0 +1,284 @@ +package qline + +// qline is a line input library for terminals that utilize +// vt100 compatible escape sequences +// +// Copyright © 2021 Brian Evans +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the “Software”), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +// KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +const ( + UpArrow rune = iota - 20 + DownArrow + LeftArrow + RightArrow + Delete + Home + End + PageUp + PageDown + Escape rune = 27 + NewLine rune = 10 + CarriageReturn rune = 13 + BackSpace rune = 127 +) + +var ( + width int +) + +type buffer struct { + buf []rune + cursor int + maxWidth int + offset int + cursorStart int + prompt string +} + +func GetInput(prompt string, content string, cols int) string { + b := buffer{make([]rune, 0, (len(content)+1)*2), 0, cols-len(prompt), 0, 0, prompt} + + var ch rune + var err error + reader := bufio.NewReader(os.Stdin) + + // b.seedContent(content) + // b.printBuf() + + for { + ch, err = ReadKey(reader) + if err != nil { + continue + } + if ch == CarriageReturn || ch == NewLine { + break + } + + if isControl(ch) { + b.controlInput(ch) + } else { + b.addChar(ch, true) + } + } + return b.string() +} + + +func (lb buffer) string() string { + return string(lb.buf) +} + +func (lb *buffer) deleteChar() { + if lb.cursor == len(lb.buf) { + return + } else if lb.cursor == len(lb.buf)-1 { + lb.buf = lb.buf[:len(lb.buf)-1] + } else { + lb.buf = append(lb.buf[:lb.cursor], lb.buf[lb.cursor+1:]...) + } + if lb.offset > 0 { + lb.offset-- + } +} + +func (lb *buffer) addChar(c rune, echo bool) { + if c < 9 || (c > 10 && c < 13) || (c > 13 && c < 32) { + return + } + if lb.cursor == len(lb.buf) { + lb.buf = append(lb.buf, c) + lb.cursor++ + } else { + lb.buf = append(lb.buf[:lb.cursor+1], lb.buf[lb.cursor:]...) + lb.buf[lb.cursor] = c + lb.cursor++ + } + if lb.cursor - lb.offset > lb.maxWidth { + lb.offset++ + } + if echo { + lb.printBuf() + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func (lb buffer) printBuf() { + out := lb.buf[lb.offset:min(len(lb.buf), lb.offset+lb.maxWidth)] + fmt.Printf("\r%s%s\033[0K\r\033[%dC", lb.prompt, string(out), lb.cursor - lb.offset + len(lb.prompt)) +} + +func (lb *buffer) controlInput(c rune) { + switch c { + case Delete: + lb.deleteChar() + case BackSpace: + if lb.cursor > 0 { + lb.cursor-- + lb.deleteChar() + } + case LeftArrow: + if lb.offset > 0 && lb.cursor - lb.offset == 0 { + lb.offset-- + } + if lb.cursor > 0 { + lb.cursor-- + } + case RightArrow: + // This is still mildly funky, but works enough + for ;lb.cursor - lb.offset >= lb.maxWidth && lb.cursor < len(lb.buf); { + lb.offset++ + } + if lb.cursor < len(lb.buf) { + lb.cursor++ + } + case Home: + lb.offset = 0 + lb.cursor = 0 + case End: + lb.cursor = len(lb.buf) + lb.offset = max(lb.cursor - lb.maxWidth, 0) + } + lb.printBuf() +} + +func (lb *buffer) seedContent(s string) { + for _, r := range s { + lb.addChar(r, false) + } +} + +func parseCursorPosition(esc string) (int, int, error) { + var row, col int + r := strings.NewReader(esc) + _, err := fmt.Fscanf(r, "\033[%d;%dR", &row, &col) + return row, col, err +} + + +func ReadKey(reader *bufio.Reader) (rune, error) { + char, _, err := reader.ReadRune() + if err != nil { + return 0, err + } + avail := reader.Buffered() + if char == Escape && avail > 0 { + var b strings.Builder + b.WriteRune(27) + for ; avail > 0; avail-- { + c, _, e := reader.ReadRune() + if e != nil { + break + } + b.WriteRune(c) + } + escSeq := b.String() + switch true { + case escSeq == "\033[A": + char = UpArrow + case escSeq == "\033[B": + char = DownArrow + case escSeq == "\033[C": + char = RightArrow + case escSeq == "\033[D": + char = LeftArrow + case escSeq == "\033[5~": + char = PageUp + case escSeq == "\033[6~": + char = PageDown + case isHomeKey(escSeq): + char = Home + case isEndKey(escSeq): + char = End + case isDeleteKey(escSeq): + char = Delete + case escSeq[len(escSeq)-1] == 'R': + // This is a request for cursor position + _, cols, err := parseCursorPosition(escSeq) + if err == nil { + err = fmt.Errorf("response") + } + return rune(cols), err + } + } + return char, nil +} + +func isControl(c rune) bool { + switch c { + case UpArrow, DownArrow, RightArrow, LeftArrow, PageUp, PageDown, Home, End, Delete, BackSpace, CarriageReturn, NewLine, Escape: + return true + default: + return false + } +} + +func isDeleteKey(seq string) bool { + switch seq { + case "\033[3~", "\033[P": + return true + default: + return false + } +} + +func isHomeKey(seq string) bool { + switch seq { + case "\033[1~", "\033[7~", "\033[H", "\033OH": + return true + default: + return false + } +} + +func isEndKey(seq string) bool { + switch seq { + case "\033[4~", "\033[8~", "\033[F", "\033OF": + return true + default: + return false + } +} + diff --git a/termios/consts_linux.go b/termios/consts_linux.go new file mode 100644 index 0000000..ae6e076 --- /dev/null +++ b/termios/consts_linux.go @@ -0,0 +1,10 @@ +// +build linux + +package termios + +import "syscall" + +const ( + getTermiosIoctl = syscall.TCGETS + setTermiosIoctl = syscall.TCSETS +) diff --git a/termios/consts_nonlinux.go b/termios/consts_nonlinux.go new file mode 100644 index 0000000..ca0daf7 --- /dev/null +++ b/termios/consts_nonlinux.go @@ -0,0 +1,10 @@ +// +build !linux + +package termios + +import "syscall" + +const ( + getTermiosIoctl = syscall.TIOCGETA + setTermiosIoctl = syscall.TIOCSETAF +) diff --git a/termios/termios.go b/termios/termios.go new file mode 100644 index 0000000..1e77f8f --- /dev/null +++ b/termios/termios.go @@ -0,0 +1,73 @@ +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 syscall.Termios +var initialSet bool = false + +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 SetInitialTermios() { + if !initialSet { + initial = getTermios() + initialSet = true + } +} + +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) +}