// 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) } }