turbo-editors/turbo-corepublic Fork 0
v0.9.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

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

project.go · 264 lines · 10.0 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1// Project settings: the editor's own directory, which a project keeps its
2// editor settings in, and the menu items that create and open it.
3
4package app
5
6import (
7 "errors"
8 "os"
9 "path/filepath"
10
11 "codeberg.org/turbo-editors/turbo-core/settings"
12 "codeberg.org/turbo-editors/turbo-core/snippets"
13 "codeberg.org/turbo-editors/turbo-core/tools"
14 "codeberg.org/turbo-editors/turbo-core/ui"
15)
16
17// CreateProjectSettings writes settings.toml into the editor's own directory
18// in the working directory, filled in with the theme currently in use, and opens it.
19//
20// The file is opened rather than merely created because a settings file is
21// only useful once it has been read: creating it and saying nothing would
22// leave the user to guess what is in it. A project that already has one is not
23// overwritten — it is opened, which is what someone choosing this menu item
24// twice almost certainly wanted.
25func (a *App) CreateProjectSettings() {
26 create := func(project string) (string, error) { return settings.Create(a.profile, project, a.themeName) }
27 if path, ok := a.createProjectFile("Project settings", create, settings.ErrExists); ok {
28 a.settingsPath = path
29 }
30}
31
32// createProjectFile writes one of the project's files in the editor's own
33// directory, reports
34// what happened on the status bar, and opens it.
35//
36// The three files — settings, snippets, tools — are created by the same dance:
37// find the working directory, write unless it is already there, say which
38// happened, open it either way. It is written once because the second copy of
39// it was already one too many.
40//
41// It returns the path and whether there is now a file there, which is true both
42// for one just created and for one that already existed: a caller wanting to
43// remember the path wants it in both cases.
44func (a *App) createProjectFile(title string, create func(string) (string, error), alreadyThere error) (string, bool) {
45 project, err := os.Getwd()
46 if err != nil {
47 a.ShowMessage(title, "Cannot tell which directory this is:\n"+err.Error())
48 return "", false
49 }
50
51 path, err := create(project)
52 switch {
53 case errors.Is(err, alreadyThere):
54 a.Message("Already there: " + a.shortPath(path))
55 case err != nil:
56 a.ShowMessage(title, err.Error())
57 return "", false
58 default:
59 a.Message("Created " + a.shortPath(path))
60 }
61
62 a.Open(path)
63 return path, true
64}
65
66// projectFile describes one of the three files a project keeps in the editor's
67// own directory, in the terms the menus need: what to call it, where the item
68// that writes it lives, and how to find it.
69//
70// The three behave identically — create it, open it, grey out whichever of the
71// two does not apply — so the behaviour is written once and the differences are
72// values.
73type projectFile struct {
74 // title heads the dialog shown when there is nothing to open.
75 title string
76 // relative is how the file is named to a person: ".turbo-go/tools.toml".
77 relative string
78 // creator is the menu path of the item that writes it, so that the dialog
79 // can say where to go rather than only what is missing.
80 creator string
81 exists func(projectDir string) bool
82 path func(projectDir string) string
83}
84
85// settingsFile, toolsFile and snippetsFile are the three of them. They are
86// methods rather than package-level values because every path is built from the
87// profile, which is per editor.
88func (a *App) settingsFile() projectFile {
89 return projectFile{
90 title: "Project settings",
91 relative: filepath.Join(a.profile.ProjectDir(), settings.FileName),
92 creator: "Options ▸ Create project settings",
93 exists: func(dir string) bool { return settings.Exists(a.profile, dir) },
94 path: func(dir string) string { return settings.Path(a.profile, dir) },
95 }
96}
97
98func (a *App) toolsFile() projectFile {
99 return projectFile{
100 title: a.toolsMenuName(),
101 relative: filepath.Join(a.profile.ProjectDir(), tools.FileName),
102 creator: a.toolsMenuName() + " ▸ " + ui.PlainLabel(createToolsLabel),
103 exists: func(dir string) bool { return tools.Exists(a.profile, dir) },
104 path: func(dir string) string { return tools.Path(a.profile, dir) },
105 }
106}
107
108func (a *App) snippetsFile() projectFile {
109 return projectFile{
110 title: "Snippets",
111 relative: filepath.Join(a.profile.ProjectDir(), snippets.FileName),
112 creator: "Snippets ▸ " + ui.PlainLabel(createSnippetsLabel),
113 exists: func(dir string) bool { return snippets.Exists(a.profile, dir) },
114 path: func(dir string) string { return snippets.ProjectPath(a.profile, dir) },
115 }
116}
117
118// openProjectFile opens one of the project's own files for editing.
119//
120// A project without one is told so rather than having one made for it: creating
121// is a separate item, so that choosing "open" can never put a directory into
122// somebody's repository by itself. The item is greyed out in that case anyway —
123// the dialog is for the caller that is not a menu.
124func (a *App) openProjectFile(file projectFile) {
125 project, err := os.Getwd()
126 if err != nil || !file.exists(project) {
127 a.ShowMessage(file.title, "This project has no "+file.relative+" yet.\n\n"+file.creator+" makes one.")
128 return
129 }
130 a.Open(file.path(project))
131}
132
133// hasProjectFile reports whether the project has one of them.
134func (a *App) hasProjectFile(file projectFile) bool {
135 project, err := os.Getwd()
136 return err == nil && file.exists(project)
137}
138
139// OpenProjectSettings opens the project's settings file for editing.
140//
141// a.OpenProjectSettings() // opens .turbo-go/settings.toml
142func (a *App) OpenProjectSettings() { a.openProjectFile(a.settingsFile()) }
143
144// OpenTools opens the project's tools file for editing.
145//
146// a.OpenTools() // opens .turbo-go/tools.toml
147func (a *App) OpenTools() { a.openProjectFile(a.toolsFile()) }
148
149// OpenSnippets opens the project's snippets file for editing.
150//
151// The project's, not the user's: this is the file the item beside it creates,
152// and a menu that sometimes opened one file and sometimes another would be a
153// menu nobody could predict.
154//
155// a.OpenSnippets() // opens .turbo-go/snippets.toml
156func (a *App) OpenSnippets() { a.openProjectFile(a.snippetsFile()) }
157
158// HasProjectSettings, HasProjectTools and HasProjectSnippets report whether the
159// project has each file. They are what decide, for every one of the six menu
160// items, which of the pair is greyed out: you can create the file you have not
161// got, and open the one you have.
162//
163// {Label: "~C~reate tools file", Enabled: not(a.HasProjectTools)}
164// {Label: "~O~pen tools file", Enabled: a.HasProjectTools}
165func (a *App) HasProjectSettings() bool { return a.hasProjectFile(a.settingsFile()) }
166
167func (a *App) HasProjectTools() bool { return a.hasProjectFile(a.toolsFile()) }
168
169func (a *App) HasProjectSnippets() bool { return a.hasProjectFile(a.snippetsFile()) }
170
171// not turns a condition into its opposite, for the menu items that are enabled
172// exactly when their partner is not.
173func not(condition func() bool) func() bool {
174 return func() bool { return !condition() }
175}
176
177// reapplySettings re-reads the project's settings when the file just written is
178// them, so that a change takes effect at once rather than at the next start-up.
179//
180// Editing settings.toml in the editor and saving it used to do nothing until
181// the editor was restarted: UseSettings was called once, from main, and nothing
182// called it again. The file is recognised by its path rather than by a
183// remembered handle, so that *creating* it counts too — a project that had no
184// settings file has no path remembered.
185//
186// Only autosave is re-applied. The theme is deliberately left alone: Options ▸
187// Theme is the live way to change it and already writes the choice back into
188// this file, and a -theme flag given on the command line is the more explicit
189// statement of the two for the session it was given in.
190func (a *App) reapplySettings(path string) {
191 project, err := os.Getwd()
192 if err != nil || !samePath(path, settings.Path(a.profile, project)) {
193 return
194 }
195
196 loaded, err := settings.Load(a.profile, project)
197 if err != nil {
198 // Saved but not in force, which is a different thing from either
199 // "saved" or "cannot save", and the only one of the three that leaves
200 // the editor behaving unlike the file in front of you.
201 a.Message("Saved, but not applied: " + err.Error())
202 return
203 }
204
205 a.settingsPath = settings.Path(a.profile, project)
206 a.SetAutosave(loaded.Autosave, loaded.AutosaveDelay)
207 a.Message("Applied " + a.shortPath(path) + " — " + describeAutosave(loaded))
208}
209
210// describeAutosave says what a settings file has just asked for, in the few
211// words a status bar has room for.
212//
213// describeAutosave(settings.Settings{Autosave: true, AutosaveDelay: 2 * time.Second})
214// // "autosave on (2s)"
215func describeAutosave(s settings.Settings) string {
216 if !s.Autosave {
217 return "autosave off"
218 }
219 return "autosave on (" + s.AutosaveDelay.String() + ")"
220}
221
222// samePath reports whether two paths name the same file, comparing them as
223// absolute paths so that "./.turbo-go/settings.toml" and the absolute spelling
224// of it are recognised as one file.
225func samePath(one, other string) bool {
226 oneAbs, err := filepath.Abs(one)
227 if err != nil {
228 return one == other
229 }
230 otherAbs, err := filepath.Abs(other)
231 if err != nil {
232 return one == other
233 }
234 return oneAbs == otherAbs
235}
236
237// rememberTheme writes the theme into the project's settings file, when the
238// project has one.
239//
240// A project with no settings file is left without one: creating it is an
241// explicit choice, and a theme picked to try it out should not put a new
242// directory into someone's repository.
243func (a *App) rememberTheme(name string) {
244 if a.settingsPath == "" {
245 return
246 }
247 if err := settings.SetTheme(a.settingsPath, name); err != nil {
248 a.Message("Theme set for this session only: " + err.Error())
249 }
250}
251
252// shortPath returns a path relative to the working directory when it is under
253// it, because ".turbo-go/settings.toml" reads better in a status bar than the
254// absolute path of the same file.
255func (a *App) shortPath(path string) string {
256 working, err := os.Getwd()
257 if err != nil {
258 return path
259 }
260 if relative, err := filepath.Rel(working, path); err == nil && !filepath.IsAbs(relative) {
261 return relative
262 }
263 return path
264}