turbo-editors/turbo-gopublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-go.git
git clone ssh://git@rickub.com/turbo-editors/turbo-go.git

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

main.go · 203 lines · 5.7 KBGo Blame HistoryRaw
📦 Turbo Go 3d7798b k33g 10h ago1// Command turbo-go is a Turbo C-style editor for Go: a full-screen terminal
2// IDE with menus, movable windows, syntax colouring and gopls completion.
3//
4// Almost all of it is turbo-core, the library every Turbo editor is built on.
5// What is here is the command line, the terminal, and internal/golang — the
6// profile that says this one is for Go.
7//
8// Usage:
9//
10// turbo-go [flags] [file...]
11//
12// Flags:
13//
14// -theme name the colour theme to start with, overriding the project's
15// -list-themes print the available themes and exit
16// -no-lsp do not start a language server
17// -version print the version and exit
18package main
19
20import (
21 "context"
22 "errors"
23 "flag"
24 "fmt"
25 "os"
26
27 "github.com/gdamore/tcell/v2"
28
29 "rickub.com/turbo-editors/turbo-core/app"
30 "rickub.com/turbo-editors/turbo-core/profile"
31 "rickub.com/turbo-editors/turbo-core/settings"
32 "rickub.com/turbo-editors/turbo-core/theme"
33 "rickub.com/turbo-editors/turbo-core/version"
34
35 "rickub.com/turbo-editors/turbo-go/internal/golang"
36)
37
38func main() {
39 if err := run(); err != nil {
40 fmt.Fprintf(os.Stderr, "%s: %v\n", golang.Slug, err)
41 os.Exit(1)
42 }
43}
44
45// options are what the command line asked for.
46type options struct {
47 theme string
48 listThemes bool
49 noLSP bool
50 version bool
51 files []string
52}
53
54// parseFlags reads the command line.
55func parseFlags() options {
56 var opts options
57
58 // The default is empty rather than the theme's name so that "was -theme
59 // given?" can still be answered afterwards, which is what lets the project
60 // settings fill it in without overriding an explicit choice.
61 flag.StringVar(&opts.theme, "theme", "", "colour theme to start with (default from the project, else "+theme.DefaultName+")")
62 flag.BoolVar(&opts.listThemes, "list-themes", false, "print the available themes and exit")
63 flag.BoolVar(&opts.noLSP, "no-lsp", false, "do not start a language server")
64 flag.BoolVar(&opts.version, "version", false, "print the version and exit")
65 flag.Parse()
66
67 opts.files = flag.Args()
68 return opts
69}
70
71// run does the work, so that main is nothing but error reporting.
72func run() error {
73 opts := parseFlags()
74 // Registering here rather than from an init function is what makes "this
75 // editor knows Go" a line somebody can read.
76 golang.Register()
77 p := golang.Profile()
78
79 switch {
80 case opts.version:
81 fmt.Printf("%s %s\n", p.Name, version.Current())
82 return nil
83 case opts.listThemes:
84 return listThemes(p)
85 }
86
87 return edit(opts, p)
88}
89
90// listThemes prints every theme that can be loaded, with its description.
91func listThemes(p profile.Profile) error {
92 userDir := p.ThemeDir()
93
94 for _, name := range theme.Available(userDir) {
95 loaded, err := theme.Load(name, userDir)
96 if err != nil {
97 fmt.Printf("%-16s (cannot be loaded: %v)\n", name, err)
98 continue
99 }
100 fmt.Printf("%-16s %s\n", name, loaded.Description())
101 }
102
103 if userDir != "" {
104 fmt.Printf("\nYour own themes go in %s\n", userDir)
105 }
106 return nil
107}
108
109// edit opens the terminal and runs the editor until the user leaves.
110func edit(opts options, p profile.Profile) error {
111 project, projectSettings := loadProjectSettings(p)
112
113 screen, err := newScreen()
114 if err != nil {
115 return err
116 }
117 // The screen must be given back whatever happens, or a crash leaves the
118 // terminal in raw mode with no cursor.
119 defer screen.Fini()
120
121 editor := app.New(screen, themeName(opts, projectSettings), p)
122 if settings.Exists(p, project) {
123 editor.UseSettings(projectSettings, settings.Path(p, project))
124 }
125 openFiles(editor, opts.files)
126
127 ctx, cancel := context.WithCancel(context.Background())
128 defer cancel()
129 if !opts.noLSP {
130 editor.StartLanguageServer(ctx, app.ProjectRoot(p, opts.files))
131 }
132 defer editor.Language().Stop(context.Background())
133
134 return editor.Run()
135}
136
137// loadProjectSettings reads .turbo-go/settings.toml from the working
138// directory, and returns that directory along with what it found.
139//
140// The working directory alone is looked in, with no walk up towards the root:
141// "the project" is where you started the editor, which is a rule you can hold
142// in your head. A file that is there but unreadable is reported on standard
143// error and then ignored — a broken settings file must not stop the editor
144// opening, because the editor is how you would fix it.
145func loadProjectSettings(p profile.Profile) (string, settings.Settings) {
146 project, err := os.Getwd()
147 if err != nil {
148 project = "."
149 }
150
151 loaded, err := settings.Load(p, project)
152 switch {
153 case errors.Is(err, settings.ErrNotFound):
154 return project, settings.Default()
155 case err != nil:
156 fmt.Fprintf(os.Stderr, "%s: %v\n", golang.Slug, err)
157 return project, settings.Default()
158 }
159 return project, loaded
160}
161
162// themeName decides which theme to start in.
163//
164// A -theme flag wins, because it is the more explicit statement of the two and
165// is how you try a theme without editing a file everyone shares. The project's
166// settings come next, and the built-in default last.
167func themeName(opts options, projectSettings settings.Settings) string {
168 switch {
169 case opts.theme != "":
170 return opts.theme
171 case projectSettings.Theme != "":
172 return projectSettings.Theme
173 default:
174 return theme.DefaultName
175 }
176}
177
178// newScreen opens the terminal and turns on what the editor needs from it.
179func newScreen() (tcell.Screen, error) {
180 screen, err := tcell.NewScreen()
181 if err != nil {
182 return nil, fmt.Errorf("opening the terminal: %w", err)
183 }
184 if err := screen.Init(); err != nil {
185 return nil, fmt.Errorf("initialising the terminal: %w", err)
186 }
187
188 screen.EnableMouse()
189 screen.EnablePaste()
190 return screen, nil
191}
192
193// openFiles opens the files named on the command line, or an empty window when
194// none were.
195func openFiles(editor *app.App, files []string) {
196 if len(files) == 0 {
197 editor.NewFile()
198 return
199 }
200 for _, file := range files {
201 editor.Open(file)
202 }
203}