turbo-editors/turbo-pythonpublic Fork 0
v1.0.1
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-python.git
git clone ssh://git@rickub.com/turbo-editors/turbo-python.git

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

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