• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

abs-lang / abs / 14326440454

08 Apr 2025 06:12AM UTC coverage: 68.526% (-0.2%) from 68.703%
14326440454

push

github

odino
prompt

0 of 63 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

3723 of 5433 relevant lines covered (68.53%)

89.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/terminal/terminal.go
1
package terminal
2

3
import (
4
        "crypto/rand"
5
        "fmt"
6
        "math/big"
7
        "os"
8
        "os/user"
9
        "strings"
10

11
        "github.com/abs-lang/abs/object"
12
        "github.com/abs-lang/abs/util"
13
        "github.com/charmbracelet/bubbles/textarea"
14
        "github.com/charmbracelet/bubbles/textinput"
15
        tea "github.com/charmbracelet/bubbletea"
16
        "github.com/charmbracelet/lipgloss"
17
)
18

19
// TODO
20
// history
21
// cursor up and down
22
// quit command
23
// help
24
// autocompleter
25
// navigate commands up
26
// print parse errors / generale errors correctly
27
// > noexist (for example)
28
// stdin() not working
29
// maybe only save incrementally in history https://stackoverflow.com/questions/7151261/append-to-a-file-in-go
30
// remove deprecated ioutil methods
31
// placeholder
32
// remove dependencies
33
// worth renaming repl to runner? and maybe terminal back to repl
34
// add prompt formatting tests
35

36
type (
37
        errMsg error
38
)
39

40
type Terminal struct {
41
        program *tea.Program
42
}
43

44
func (t *Terminal) Run() error {
×
45
        _, err := t.program.Run()
×
46

×
47
        return err
×
48
}
×
49

50
type Runner func(string) string
51

52
func New(user string, version string, env *object.Environment, runner Runner) *Terminal {
×
53
        return &Terminal{
×
NEW
54
                tea.NewProgram(getInitialState(user, version, env, runner)),
×
55
        }
×
56
}
×
57

58
type Model struct {
59
        user            string
60
        version         string
61
        runner          Runner
62
        env             *object.Environment
63
        prompt          func(*object.Environment) string
64
        history         []string
65
        historyPoint    int
66
        historyFile     string
67
        historyMaxLInes int
68
        dirty           string
69
        messages        []string
70
        in              textinput.Model
71
        err             error
72
}
73

NEW
74
func (m Model) Clear() (Model, tea.Cmd) {
×
NEW
75
        m.messages = []string{}
×
NEW
76
        return m, tea.ClearScreen
×
NEW
77
}
×
78

NEW
79
func (m Model) Quit() (Model, tea.Cmd) {
×
NEW
80
        saveHistory(m.historyFile, m.historyMaxLInes, m.history)
×
NEW
81

×
NEW
82
        return m, tea.Quit
×
NEW
83
}
×
84

NEW
85
func (m Model) Eval() (Model, tea.Cmd) {
×
NEW
86
        m.dirty = ""
×
NEW
87
        m.messages = append(m.messages, m.prompt(m.env)+m.in.Value())
×
NEW
88

×
NEW
89
        if m.in.Value() == "" {
×
NEW
90
                return m, nil
×
NEW
91
        }
×
92

NEW
93
        res := m.runner(m.in.Value())
×
NEW
94
        m.history = append(m.history, m.in.Value())
×
NEW
95
        m.historyPoint = len(m.history)
×
NEW
96

×
NEW
97
        if res != "" {
×
NEW
98
                m.messages = append(m.messages, res)
×
NEW
99
        }
×
100

NEW
101
        m.in.Prompt = m.prompt(m.env)
×
NEW
102
        m.in.Reset()
×
NEW
103

×
NEW
104
        return m, nil
×
105
}
106

NEW
107
func (m Model) Interrupt() (Model, tea.Cmd) {
×
NEW
108
        m.messages = append(m.messages, m.prompt(m.env)+m.in.Value())
×
NEW
109
        m.in.Reset()
×
NEW
110

×
NEW
111
        return m, nil
×
NEW
112
}
×
113

NEW
114
func getInitialState(user string, version string, env *object.Environment, r Runner) Model {
×
115
        in := textinput.New()
×
NEW
116
        in.Prompt = getPrompt(env)
×
117
        in.Placeholder = "`date`"
×
118
        historyFile, maxLines := getHistoryConfiguration(env)
×
119
        history := getHistory(historyFile, maxLines)
×
120
        in.Focus()
×
121
        // ti.CharLimit = 156
×
122
        // ti.Width = 20
×
123
        messages := []string{}
×
124
        messages = append(messages, fmt.Sprintf("Hello %s, welcome to the ABS (%s) programming language!", user, version))
×
125
        // check for new version about 10% of the time,
×
126
        // to avoid too many hangups
×
127
        if r, e := rand.Int(rand.Reader, big.NewInt(100)); e == nil && r.Int64() < 10 {
×
128
                if newver, update := util.UpdateAvailable(version); update {
×
129
                        messages = append(messages, fmt.Sprintf(
×
130
                                "*** Update available: %s (your version is %s) ***",
×
131
                                newver,
×
132
                                version,
×
133
                        ))
×
134
                }
×
135
        }
136
        messages = append(messages, "Type 'quit' when you're done, 'help' if you get lost!")
×
137

×
NEW
138
        return Model{
×
NEW
139
                user:            user,
×
NEW
140
                version:         version,
×
NEW
141
                runner:          r,
×
NEW
142
                prompt:          getPrompt,
×
NEW
143
                env:             env,
×
144
                in:              in,
×
145
                history:         history,
×
146
                historyPoint:    len(history),
×
147
                historyFile:     historyFile,
×
148
                historyMaxLInes: maxLines,
×
149
                dirty:           "",
×
150
                messages:        messages,
×
NEW
151

×
152
                err: nil,
×
153
        }
×
154
}
155

NEW
156
func (m Model) Init() tea.Cmd {
×
157
        return textarea.Blink
×
158
}
×
159

NEW
160
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
×
161
        var (
×
162
                tiCmd tea.Cmd
×
163
        )
×
164

×
165
        m.in, tiCmd = m.in.Update(msg)
×
166

×
167
        switch msg := msg.(type) {
×
168
        case tea.KeyMsg:
×
169
                switch msg.Type {
×
170
                case tea.KeyEsc, tea.KeyCtrlD:
×
NEW
171
                        return m.Quit()
×
NEW
172
                case tea.KeyCtrlC:
×
NEW
173
                        return m.Interrupt()
×
NEW
174
                case tea.KeyEnter:
×
NEW
175
                        return m.Eval()
×
UNCOV
176
                case tea.KeyCtrlL:
×
NEW
177
                        return m.Clear()
×
178
                case tea.KeyUp:
×
NEW
179
                        // oly store dirty Model on first key up
×
180
                        if m.dirty == "" {
×
181
                                m.dirty = m.in.Value()
×
182
                        }
×
183

184
                        if m.historyPoint <= 0 {
×
185
                                break
×
186
                        }
187

188
                        newPoint := m.historyPoint - 1
×
189

×
190
                        if len(m.history) < newPoint {
×
191
                                break
×
192
                        }
193

194
                        m.historyPoint = newPoint
×
195
                        m.in.SetValue(m.history[m.historyPoint])
×
196
                case tea.KeyDown:
×
197
                        newPoint := m.historyPoint + 1
×
198

×
199
                        if newPoint <= len(m.history)-1 {
×
200
                                m.historyPoint = newPoint
×
201
                                m.in.SetValue(m.history[m.historyPoint])
×
202
                                break
×
203
                        }
204

205
                        m.in.SetValue(m.dirty)
×
206
                }
207

208
        case errMsg:
×
209
                m.err = msg
×
210
                return m, nil
×
211
        }
212

213
        return m, tiCmd
×
214
}
215

NEW
216
func (m Model) View() string {
×
NEW
217
        if len(m.messages) == 0 {
×
NEW
218
                return m.in.View()
×
NEW
219
        }
×
220

221
        return fmt.Sprintf(
×
222
                "%s\n%s",
×
NEW
223
                strings.Join(m.messages, "\n"),
×
224
                m.in.View(),
×
225
        )
×
226
}
227

NEW
228
func getPrompt(env *object.Environment) string {
×
NEW
229
        prompt := util.GetEnvVar(env, "ABS_PROMPT_PREFIX", ABS_DEFAULT_PROMPT)
×
NEW
230
        prompt = lipgloss.NewStyle().Foreground(lipgloss.Color("#4287f5")).Render(prompt)
×
231
        livePrompt := util.GetEnvVar(env, "ABS_PROMPT_LIVE_PREFIX", "false")
×
NEW
232

×
233
        if livePrompt == "true" {
×
NEW
234
                return formatLivePrefix(prompt)
×
235
        }
×
236

NEW
237
        return prompt
×
238
}
239

240
// format ABS_PROMPT_PREFIX = "{user}@{host}:{dir} $"
241
func formatLivePrefix(prefix string) string {
×
242
        livePrefix := prefix
×
243
        if strings.Contains(prefix, "{") {
×
244
                userInfo, _ := user.Current()
×
245
                user := userInfo.Username
×
246
                host, _ := os.Hostname()
×
247
                dir, _ := os.Getwd()
×
248
                // shorten homedir to ~/
×
249
                homeDir := userInfo.HomeDir
×
250
                dir = strings.Replace(dir, homeDir, "~", 1)
×
251
                // format the livePrefix
×
252
                livePrefix = strings.Replace(livePrefix, "{user}", user, 1)
×
253
                livePrefix = strings.Replace(livePrefix, "{host}", host, 1)
×
254
                livePrefix = strings.Replace(livePrefix, "{dir}", dir, 1)
×
255
        }
×
256
        return livePrefix
×
257
}
258

259
// support for user config of ABS REPL prompt string
260
var ABS_DEFAULT_PROMPT = "> "
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc