aboutsummaryrefslogtreecommitdiffstats
path: root/ui.go
blob: bd39eaf0562e3e2dc4e9d3d3a75d84efa51c7d63 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package main

import (
	"fmt"

	"github.com/charmbracelet/bubbletea"
)

// UTF-8 box drawing characters.
const (
	boxHorizontal  = '─'
	boxVertical    = '│'
	boxTopLeft     = '┌'
	boxTopRight    = '┐'
	boxBottomLeft  = '└'
	boxBottomRight = '┘'
)

// sigDigs is the number of significant digits when printing a number.
const sigDigs = 17

// maxWidth is the maximum width that the window will grow to.
const maxWidth = 32

type UI struct {
	calc   Calculator
	width  int // Width of the window measured in characters.
	height int
}

func (ui UI) Init() tea.Cmd {
	return nil
}

func (ui UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		ui.width = msg.Width
		ui.height = msg.Height
	case tea.KeyMsg:
		switch msg.String() {
		case "ctrl+c", "Q":
			return ui, tea.Quit
		case "J", "K":
			ui.calc.swap()
		case "D":
			ui.calc.buf = ""
		case "C":
			ui.calc.buf = ""
			ui.calc.stack = ui.calc.stack[:0]
		case "A":
			ui.calc.angleMode = !ui.calc.angleMode
		case "N":
			ui.calc.negate()
		case "+", "-", "*", "/", "%", "^":
			operator := msg.String()[0]
			if err := ui.calc.performOperation(operator); err != nil {
				panic(err)
			}
		case "backspace":
			if len(ui.calc.buf) > 0 {
				ui.calc.buf = ui.calc.buf[:len(ui.calc.buf)-1]
			}
		case "enter":
			if fn := parseFunction(ui.calc.buf); fn != nil {
				fn(&ui.calc)
			} else if v, err := ui.calc.parseBuffer(); err == nil {
				ui.calc.stack.push(v)
			}
			ui.calc.buf = ""
		default:
			ui.calc.buf += msg.String()
		}
	}
	return ui, nil
}

func (ui UI) View() string {
	s := padding(ui)

	width := min(ui.width, maxWidth)

	// Angle mode.
	s += fmt.Sprintf("%*s\n", width-1, ui.calc.angleMode)

	// Stack.
	top := boxTop(width)
	bottom := boxBottom(width)
	s += top + "\n"
	for _, f := range ui.calc.stack {
		s += fmt.Sprintf("%[1]c%[2]*.[2]*s%[1]c\n", boxVertical, width-2, printNum(f))
	}
	s += bottom + "\n"

	// Buffer.
	s += top + "\n"
	s += fmt.Sprintf("%[1]c>%[2]*.[2]*s%[1]c\n", boxVertical, width-3, ui.calc.buf)
	s += bottom
	return s
}

func padding(ui UI) string {
	var ( // Number of lines occupied by each ui element.
		angleMode = 1
		stack     = len(ui.calc.stack) + 2
		buf       = 3
	)
	padlines := ui.height - angleMode - stack - buf
	if padlines < 1 {
		return ""
	}
	s := make([]byte, padlines)
	for i := 0; i < padlines; i++ {
		s[i] = '\n'
	}
	return string(s)
}

func printNum(v float64) string {
	return fmt.Sprintf("%.*g", sigDigs, v)
}

// boxTop returns the top of a UTF-8 box, 'width' characters wide (including
// corners).
func boxTop(width int) string {
	if width < 1 {
		return ""
	}
	row := make([]rune, width)
	row[0] = boxTopLeft
	row[width-1] = boxTopRight
	if width > 1 {
		fill(row[1:width-1], boxHorizontal)
	}
	return string(row)
}

// boxBottom returns the botom of a UTF-8 box, 'width' characters wide
// (including corners).
func boxBottom(width int) string {
	if width < 1 {
		return ""
	}
	row := make([]rune, width)
	row[0] = boxBottomLeft
	row[width-1] = boxBottomRight
	if width > 1 {
		fill(row[1:width-1], boxHorizontal)
	}
	return string(row)
}

// fill fills s with c.
func fill(s []rune, c rune) {
	for i := range s {
		s[i] = c
	}
}