turbo-editors/turbo-moonbitpublic Fork 0
ded61deede2aab942065647372984c8d3179037b
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-moonbit.git
git clone ssh://git@rickub.com/turbo-editors/turbo-moonbit.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

main.go · 242 lines · 7.3 KBGo Blame HistoryRaw
📦 Turbo MoonBit cc1f595 k33g yesterday1// Command turbo-moonbit is a Turbo C-style editor for MoonBit: a full-screen
2// terminal IDE with menus, movable windows, syntax colouring and completion
3// from moon-lsp.
4//
5// Almost all of it is turbo-core, the library every Turbo editor is built on.
6// What is here is the command line, the terminal, and internal/moonbitlang —
7// the profile that says this one is for MoonBit.
8//
9// Usage:
10//
11// turbo-moonbit [flags] [file...]
12//
13// Flags:
14//
15// -theme name the colour theme to start with, overriding the project's
16// -list-themes print the available themes and exit
17// -no-lsp do not start a language server
18// -version print the version and exit
📦 Turbo MoonBit ded61de k33g 8h ago19// -load-config url copy the .turbo-moonbit directory found at a repository URL into the working directory and exit
📦 Turbo MoonBit cc1f595 k33g yesterday20package main
21
22import (
23 "context"
24 "errors"
25 "flag"
26 "fmt"
27 "os"
📦 Turbo MoonBit ded61de k33g 8h ago28 "strings"
📦 Turbo MoonBit cc1f595 k33g yesterday29
30 "github.com/gdamore/tcell/v2"
31
32 "rickub.com/turbo-editors/turbo-core/app"
📦 Turbo MoonBit ded61de k33g 8h ago33 "rickub.com/turbo-editors/turbo-core/configrepo"
📦 Turbo MoonBit cc1f595 k33g yesterday34 "rickub.com/turbo-editors/turbo-core/profile"
35 "rickub.com/turbo-editors/turbo-core/settings"
36 "rickub.com/turbo-editors/turbo-core/theme"
37 "rickub.com/turbo-editors/turbo-core/version"
38
39 "rickub.com/turbo-editors/turbo-moonbit/internal/moonbitlang"
40)
41
42func main() {
43 if err := run(); err != nil {
44 fmt.Fprintf(os.Stderr, "%s: %v\n", moonbitlang.Slug, err)
45 os.Exit(1)
46 }
47}
48
49// options are what the command line asked for.
50type options struct {
51 theme string
52 listThemes bool
📦 Turbo MoonBit ded61de k33g 8h ago53 loadConfig string
📦 Turbo MoonBit cc1f595 k33g yesterday54 noLSP bool
55 version bool
56 files []string
57}
58
59// parseFlags reads the command line.
60func parseFlags() options {
61 var opts options
62
63 // The default is empty rather than the theme's name so that "was -theme
64 // given?" can still be answered afterwards, which is what lets the project
65 // settings fill it in without overriding an explicit choice.
66 flag.StringVar(&opts.theme, "theme", "", "colour theme to start with (default from the project, else "+theme.DefaultName+")")
67 flag.BoolVar(&opts.listThemes, "list-themes", false, "print the available themes and exit")
68 flag.BoolVar(&opts.noLSP, "no-lsp", false, "do not start a language server")
69 flag.BoolVar(&opts.version, "version", false, "print the version and exit")
📦 Turbo MoonBit ded61de k33g 8h ago70 flag.StringVar(&opts.loadConfig, "load-config", "", "copy the .turbo-moonbit directory found at this repository URL into the working directory, then exit")
📦 Turbo MoonBit cc1f595 k33g yesterday71 flag.Parse()
72
73 opts.files = flag.Args()
74 return opts
75}
76
77// run does the work, so that main is nothing but error reporting.
78func run() error {
79 opts := parseFlags()
80 // Registering here rather than from an init function is what makes "this
81 // editor knows MoonBit" a line somebody can read.
82 moonbitlang.Register()
83 p := moonbitlang.Profile()
84
85 switch {
86 case opts.version:
87 fmt.Printf("%s %s\n", p.Name, version.Current())
88 return nil
89 case opts.listThemes:
90 return listThemes(p)
📦 Turbo MoonBit ded61de k33g 8h ago91 case opts.loadConfig != "":
92 return loadConfig(p, opts.loadConfig)
📦 Turbo MoonBit cc1f595 k33g yesterday93 }
94
95 return edit(opts, p)
96}
97
98// listThemes prints every theme that can be loaded, with its description.
99func listThemes(p profile.Profile) error {
100 userDir := p.ThemeDir()
101
102 for _, name := range theme.Available(userDir) {
103 loaded, err := theme.Load(name, userDir)
104 if err != nil {
105 fmt.Printf("%-16s (cannot be loaded: %v)\n", name, err)
106 continue
107 }
108 fmt.Printf("%-16s %s\n", name, loaded.Description())
109 }
110
111 if userDir != "" {
112 fmt.Printf("\nYour own themes go in %s\n", userDir)
113 }
114 return nil
115}
116
📦 Turbo MoonBit ded61de k33g 8h ago117// loadConfig copies a shared configuration — the .turbo-moonbit directory at a
118// repository URL, on rickub, GitHub, GitLab or Codeberg — into the working
119// directory, and says what arrived. The editor is not started: this is a
120// set-up step, and the next command is the one that opens the project.
121//
122// A project that already has a .turbo-moonbit is left alone; turbo-core refuses
123// rather than overwrite what the project decided.
124func loadConfig(p profile.Profile, url string) error {
125 result, err := configrepo.Load(context.Background(), p, url, ".")
126 if err != nil {
127 return err
128 }
129 fmt.Print(describeLoad(result))
130 return nil
131}
132
133// describeLoad is what a successful -load-config prints: where the files
134// came from, and each of them, so the reader knows what is now in force.
135func describeLoad(result configrepo.Result) string {
136 var out strings.Builder
137 fmt.Fprintf(&out, "Copied %s from %s", result.Dir, result.Remote)
138 if result.Ref != "" {
139 fmt.Fprintf(&out, " (%s)", result.Ref)
140 }
141 out.WriteString(":\n")
142 for _, file := range result.Files {
143 fmt.Fprintf(&out, " %s\n", file)
144 }
145 return out.String()
146}
147
📦 Turbo MoonBit cc1f595 k33g yesterday148// edit opens the terminal and runs the editor until the user leaves.
149func edit(opts options, p profile.Profile) error {
150 project, projectSettings := loadProjectSettings(p)
151
152 screen, err := newScreen()
153 if err != nil {
154 return err
155 }
156 // The screen must be given back whatever happens, or a crash leaves the
157 // terminal in raw mode with no cursor.
158 defer screen.Fini()
159
160 editor := app.New(screen, themeName(opts, projectSettings), p)
161 if settings.Exists(p, project) {
162 editor.UseSettings(projectSettings, settings.Path(p, project))
163 }
164 openFiles(editor, opts.files)
165
166 ctx, cancel := context.WithCancel(context.Background())
167 defer cancel()
168 if !opts.noLSP {
169 editor.StartLanguageServer(ctx, app.ProjectRoot(p, opts.files))
170 }
171 defer editor.Language().Stop(context.Background())
172
173 return editor.Run()
174}
175
176// loadProjectSettings reads .turbo-moonbit/settings.toml from the working
177// directory, and returns that directory along with what it found.
178//
179// The working directory alone is looked in, with no walk up towards the root:
180// "the project" is where you started the editor, which is a rule you can hold
181// in your head. A file that is there but unreadable is reported on standard
182// error and then ignored — a broken settings file must not stop the editor
183// opening, because the editor is how you would fix it.
184func loadProjectSettings(p profile.Profile) (string, settings.Settings) {
185 project, err := os.Getwd()
186 if err != nil {
187 project = "."
188 }
189
190 loaded, err := settings.Load(p, project)
191 switch {
192 case errors.Is(err, settings.ErrNotFound):
193 return project, settings.Default()
194 case err != nil:
195 fmt.Fprintf(os.Stderr, "%s: %v\n", moonbitlang.Slug, err)
196 return project, settings.Default()
197 }
198 return project, loaded
199}
200
201// themeName decides which theme to start in.
202//
203// A -theme flag wins, because it is the more explicit statement of the two and
204// is how you try a theme without editing a file everyone shares. The project's
205// settings come next, and the built-in default last.
206func themeName(opts options, projectSettings settings.Settings) string {
207 switch {
208 case opts.theme != "":
209 return opts.theme
210 case projectSettings.Theme != "":
211 return projectSettings.Theme
212 default:
213 return theme.DefaultName
214 }
215}
216
217// newScreen opens the terminal and turns on what the editor needs from it.
218func newScreen() (tcell.Screen, error) {
219 screen, err := tcell.NewScreen()
220 if err != nil {
221 return nil, fmt.Errorf("opening the terminal: %w", err)
222 }
223 if err := screen.Init(); err != nil {
224 return nil, fmt.Errorf("initialising the terminal: %w", err)
225 }
226
227 screen.EnableMouse()
228 screen.EnablePaste()
229 return screen, nil
230}
231
232// openFiles opens the files named on the command line, or an empty window when
233// none were.
234func openFiles(editor *app.App, files []string) {
235 if len(files) == 0 {
236 editor.NewFile()
237 return
238 }
239 for _, file := range files {
240 editor.Open(file)
241 }
242}