turbo-editors/turbo-moonbitpublic Fork 0
8c683ec34820eacb48a5069d622bb61461e7abed
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.

📦 Turbo MoonBit ded61de · on 8c683ec34820eacb48a5069d622bb61461e7abed · k33g · 10h ago
main.go · 242 lines · 7.3 KBGo Blame HistoryRaw
  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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
// Command turbo-moonbit is a Turbo C-style editor for MoonBit: a full-screen
// terminal IDE with menus, movable windows, syntax colouring and completion
// from moon-lsp.
//
// Almost all of it is turbo-core, the library every Turbo editor is built on.
// What is here is the command line, the terminal, and internal/moonbitlang —
// the profile that says this one is for MoonBit.
//
// Usage:
//
//	turbo-moonbit [flags] [file...]
//
// Flags:
//
//	-theme name   the colour theme to start with, overriding the project's
//	-list-themes  print the available themes and exit
//	-no-lsp       do not start a language server
//	-version      print the version and exit
//	-load-config url  copy the .turbo-moonbit directory found at a repository URL into the working directory and exit
package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"os"
	"strings"

	"github.com/gdamore/tcell/v2"

	"rickub.com/turbo-editors/turbo-core/app"
	"rickub.com/turbo-editors/turbo-core/configrepo"
	"rickub.com/turbo-editors/turbo-core/profile"
	"rickub.com/turbo-editors/turbo-core/settings"
	"rickub.com/turbo-editors/turbo-core/theme"
	"rickub.com/turbo-editors/turbo-core/version"

	"rickub.com/turbo-editors/turbo-moonbit/internal/moonbitlang"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintf(os.Stderr, "%s: %v\n", moonbitlang.Slug, err)
		os.Exit(1)
	}
}

// options are what the command line asked for.
type options struct {
	theme      string
	listThemes bool
	loadConfig string
	noLSP      bool
	version    bool
	files      []string
}

// parseFlags reads the command line.
func parseFlags() options {
	var opts options

	// The default is empty rather than the theme's name so that "was -theme
	// given?" can still be answered afterwards, which is what lets the project
	// settings fill it in without overriding an explicit choice.
	flag.StringVar(&opts.theme, "theme", "", "colour theme to start with (default from the project, else "+theme.DefaultName+")")
	flag.BoolVar(&opts.listThemes, "list-themes", false, "print the available themes and exit")
	flag.BoolVar(&opts.noLSP, "no-lsp", false, "do not start a language server")
	flag.BoolVar(&opts.version, "version", false, "print the version and exit")
	flag.StringVar(&opts.loadConfig, "load-config", "", "copy the .turbo-moonbit directory found at this repository URL into the working directory, then exit")
	flag.Parse()

	opts.files = flag.Args()
	return opts
}

// run does the work, so that main is nothing but error reporting.
func run() error {
	opts := parseFlags()
	// Registering here rather than from an init function is what makes "this
	// editor knows MoonBit" a line somebody can read.
	moonbitlang.Register()
	p := moonbitlang.Profile()

	switch {
	case opts.version:
		fmt.Printf("%s %s\n", p.Name, version.Current())
		return nil
	case opts.listThemes:
		return listThemes(p)
	case opts.loadConfig != "":
		return loadConfig(p, opts.loadConfig)
	}

	return edit(opts, p)
}

// listThemes prints every theme that can be loaded, with its description.
func listThemes(p profile.Profile) error {
	userDir := p.ThemeDir()

	for _, name := range theme.Available(userDir) {
		loaded, err := theme.Load(name, userDir)
		if err != nil {
			fmt.Printf("%-16s (cannot be loaded: %v)\n", name, err)
			continue
		}
		fmt.Printf("%-16s %s\n", name, loaded.Description())
	}

	if userDir != "" {
		fmt.Printf("\nYour own themes go in %s\n", userDir)
	}
	return nil
}

// loadConfig copies a shared configuration — the .turbo-moonbit directory at a
// repository URL, on rickub, GitHub, GitLab or Codeberg — into the working
// directory, and says what arrived. The editor is not started: this is a
// set-up step, and the next command is the one that opens the project.
//
// A project that already has a .turbo-moonbit is left alone; turbo-core refuses
// rather than overwrite what the project decided.
func loadConfig(p profile.Profile, url string) error {
	result, err := configrepo.Load(context.Background(), p, url, ".")
	if err != nil {
		return err
	}
	fmt.Print(describeLoad(result))
	return nil
}

// describeLoad is what a successful -load-config prints: where the files
// came from, and each of them, so the reader knows what is now in force.
func describeLoad(result configrepo.Result) string {
	var out strings.Builder
	fmt.Fprintf(&out, "Copied %s from %s", result.Dir, result.Remote)
	if result.Ref != "" {
		fmt.Fprintf(&out, " (%s)", result.Ref)
	}
	out.WriteString(":\n")
	for _, file := range result.Files {
		fmt.Fprintf(&out, "  %s\n", file)
	}
	return out.String()
}

// edit opens the terminal and runs the editor until the user leaves.
func edit(opts options, p profile.Profile) error {
	project, projectSettings := loadProjectSettings(p)

	screen, err := newScreen()
	if err != nil {
		return err
	}
	// The screen must be given back whatever happens, or a crash leaves the
	// terminal in raw mode with no cursor.
	defer screen.Fini()

	editor := app.New(screen, themeName(opts, projectSettings), p)
	if settings.Exists(p, project) {
		editor.UseSettings(projectSettings, settings.Path(p, project))
	}
	openFiles(editor, opts.files)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	if !opts.noLSP {
		editor.StartLanguageServer(ctx, app.ProjectRoot(p, opts.files))
	}
	defer editor.Language().Stop(context.Background())

	return editor.Run()
}

// loadProjectSettings reads .turbo-moonbit/settings.toml from the working
// directory, and returns that directory along with what it found.
//
// The working directory alone is looked in, with no walk up towards the root:
// "the project" is where you started the editor, which is a rule you can hold
// in your head. A file that is there but unreadable is reported on standard
// error and then ignored — a broken settings file must not stop the editor
// opening, because the editor is how you would fix it.
func loadProjectSettings(p profile.Profile) (string, settings.Settings) {
	project, err := os.Getwd()
	if err != nil {
		project = "."
	}

	loaded, err := settings.Load(p, project)
	switch {
	case errors.Is(err, settings.ErrNotFound):
		return project, settings.Default()
	case err != nil:
		fmt.Fprintf(os.Stderr, "%s: %v\n", moonbitlang.Slug, err)
		return project, settings.Default()
	}
	return project, loaded
}

// themeName decides which theme to start in.
//
// A -theme flag wins, because it is the more explicit statement of the two and
// is how you try a theme without editing a file everyone shares. The project's
// settings come next, and the built-in default last.
func themeName(opts options, projectSettings settings.Settings) string {
	switch {
	case opts.theme != "":
		return opts.theme
	case projectSettings.Theme != "":
		return projectSettings.Theme
	default:
		return theme.DefaultName
	}
}

// newScreen opens the terminal and turns on what the editor needs from it.
func newScreen() (tcell.Screen, error) {
	screen, err := tcell.NewScreen()
	if err != nil {
		return nil, fmt.Errorf("opening the terminal: %w", err)
	}
	if err := screen.Init(); err != nil {
		return nil, fmt.Errorf("initialising the terminal: %w", err)
	}

	screen.EnableMouse()
	screen.EnablePaste()
	return screen, nil
}

// openFiles opens the files named on the command line, or an empty window when
// none were.
func openFiles(editor *app.App, files []string) {
	if len(files) == 0 {
		editor.NewFile()
		return
	}
	for _, file := range files {
		editor.Open(file)
	}
}