turbo-editors/turbo-rustpublic Fork 0
713ea5c03c3513c6f6bc4e94969e556ff736e038
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-rust.git
git clone ssh://git@rickub.com/turbo-editors/turbo-rust.git

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

📦 Turbo Rust 713ea5c · on 713ea5c03c3513c6f6bc4e94969e556ff736e038 · k33g · 11h ago
main.go · 203 lines · 5.7 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
// Command turbo-rust is a Turbo C-style editor for Rust: a full-screen terminal
// IDE with menus, movable windows, syntax colouring and rust-analyzer completion.
//
// 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/rustlang — the
// profile that says this one is for Rust.
//
// Usage:
//
//	turbo-rust [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
package main

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

	"github.com/gdamore/tcell/v2"

	"rickub.com/turbo-editors/turbo-core/app"
	"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-rust/internal/rustlang"
)

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

// options are what the command line asked for.
type options struct {
	theme      string
	listThemes bool
	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.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 Rust" a line somebody can read.
	rustlang.Register()
	p := rustlang.Profile()

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

	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
}

// 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-rust/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", rustlang.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)
	}
}