turbo-editors/turbo-corepublic Fork 0
v1.0.2
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.

📦 Turbo Core f3ade8d · on v1.0.2 · k33g · 9h ago
project.go · 264 lines · 10.0 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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// Project settings: the editor's own directory, which a project keeps its
// editor settings in, and the menu items that create and open it.

package app

import (
	"errors"
	"os"
	"path/filepath"

	"rickub.com/turbo-editors/turbo-core/settings"
	"rickub.com/turbo-editors/turbo-core/snippets"
	"rickub.com/turbo-editors/turbo-core/tools"
	"rickub.com/turbo-editors/turbo-core/ui"
)

// CreateProjectSettings writes settings.toml into the editor's own directory
// in the working directory, filled in with the theme currently in use, and opens it.
//
// The file is opened rather than merely created because a settings file is
// only useful once it has been read: creating it and saying nothing would
// leave the user to guess what is in it. A project that already has one is not
// overwritten — it is opened, which is what someone choosing this menu item
// twice almost certainly wanted.
func (a *App) CreateProjectSettings() {
	create := func(project string) (string, error) { return settings.Create(a.profile, project, a.themeName) }
	if path, ok := a.createProjectFile("Project settings", create, settings.ErrExists); ok {
		a.settingsPath = path
	}
}

// createProjectFile writes one of the project's files in the editor's own
// directory, reports
// what happened on the status bar, and opens it.
//
// The three files — settings, snippets, tools — are created by the same dance:
// find the working directory, write unless it is already there, say which
// happened, open it either way. It is written once because the second copy of
// it was already one too many.
//
// It returns the path and whether there is now a file there, which is true both
// for one just created and for one that already existed: a caller wanting to
// remember the path wants it in both cases.
func (a *App) createProjectFile(title string, create func(string) (string, error), alreadyThere error) (string, bool) {
	project, err := os.Getwd()
	if err != nil {
		a.ShowMessage(title, "Cannot tell which directory this is:\n"+err.Error())
		return "", false
	}

	path, err := create(project)
	switch {
	case errors.Is(err, alreadyThere):
		a.Message("Already there: " + a.shortPath(path))
	case err != nil:
		a.ShowMessage(title, err.Error())
		return "", false
	default:
		a.Message("Created " + a.shortPath(path))
	}

	a.Open(path)
	return path, true
}

// projectFile describes one of the three files a project keeps in the editor's
// own directory, in the terms the menus need: what to call it, where the item
// that writes it lives, and how to find it.
//
// The three behave identically — create it, open it, grey out whichever of the
// two does not apply — so the behaviour is written once and the differences are
// values.
type projectFile struct {
	// title heads the dialog shown when there is nothing to open.
	title string
	// relative is how the file is named to a person: ".turbo-go/tools.toml".
	relative string
	// creator is the menu path of the item that writes it, so that the dialog
	// can say where to go rather than only what is missing.
	creator string
	exists  func(projectDir string) bool
	path    func(projectDir string) string
}

// settingsFile, toolsFile and snippetsFile are the three of them. They are
// methods rather than package-level values because every path is built from the
// profile, which is per editor.
func (a *App) settingsFile() projectFile {
	return projectFile{
		title:    "Project settings",
		relative: filepath.Join(a.profile.ProjectDir(), settings.FileName),
		creator:  "Options ▸ Create project settings",
		exists:   func(dir string) bool { return settings.Exists(a.profile, dir) },
		path:     func(dir string) string { return settings.Path(a.profile, dir) },
	}
}

func (a *App) toolsFile() projectFile {
	return projectFile{
		title:    a.toolsMenuName(),
		relative: filepath.Join(a.profile.ProjectDir(), tools.FileName),
		creator:  a.toolsMenuName() + " ▸ " + ui.PlainLabel(createToolsLabel),
		exists:   func(dir string) bool { return tools.Exists(a.profile, dir) },
		path:     func(dir string) string { return tools.Path(a.profile, dir) },
	}
}

func (a *App) snippetsFile() projectFile {
	return projectFile{
		title:    "Snippets",
		relative: filepath.Join(a.profile.ProjectDir(), snippets.FileName),
		creator:  "Snippets ▸ " + ui.PlainLabel(createSnippetsLabel),
		exists:   func(dir string) bool { return snippets.Exists(a.profile, dir) },
		path:     func(dir string) string { return snippets.ProjectPath(a.profile, dir) },
	}
}

// openProjectFile opens one of the project's own files for editing.
//
// A project without one is told so rather than having one made for it: creating
// is a separate item, so that choosing "open" can never put a directory into
// somebody's repository by itself. The item is greyed out in that case anyway —
// the dialog is for the caller that is not a menu.
func (a *App) openProjectFile(file projectFile) {
	project, err := os.Getwd()
	if err != nil || !file.exists(project) {
		a.ShowMessage(file.title, "This project has no "+file.relative+" yet.\n\n"+file.creator+" makes one.")
		return
	}
	a.Open(file.path(project))
}

// hasProjectFile reports whether the project has one of them.
func (a *App) hasProjectFile(file projectFile) bool {
	project, err := os.Getwd()
	return err == nil && file.exists(project)
}

// OpenProjectSettings opens the project's settings file for editing.
//
//	a.OpenProjectSettings() // opens .turbo-go/settings.toml
func (a *App) OpenProjectSettings() { a.openProjectFile(a.settingsFile()) }

// OpenTools opens the project's tools file for editing.
//
//	a.OpenTools() // opens .turbo-go/tools.toml
func (a *App) OpenTools() { a.openProjectFile(a.toolsFile()) }

// OpenSnippets opens the project's snippets file for editing.
//
// The project's, not the user's: this is the file the item beside it creates,
// and a menu that sometimes opened one file and sometimes another would be a
// menu nobody could predict.
//
//	a.OpenSnippets() // opens .turbo-go/snippets.toml
func (a *App) OpenSnippets() { a.openProjectFile(a.snippetsFile()) }

// HasProjectSettings, HasProjectTools and HasProjectSnippets report whether the
// project has each file. They are what decide, for every one of the six menu
// items, which of the pair is greyed out: you can create the file you have not
// got, and open the one you have.
//
//	{Label: "~C~reate tools file", Enabled: not(a.HasProjectTools)}
//	{Label: "~O~pen tools file", Enabled: a.HasProjectTools}
func (a *App) HasProjectSettings() bool { return a.hasProjectFile(a.settingsFile()) }

func (a *App) HasProjectTools() bool { return a.hasProjectFile(a.toolsFile()) }

func (a *App) HasProjectSnippets() bool { return a.hasProjectFile(a.snippetsFile()) }

// not turns a condition into its opposite, for the menu items that are enabled
// exactly when their partner is not.
func not(condition func() bool) func() bool {
	return func() bool { return !condition() }
}

// reapplySettings re-reads the project's settings when the file just written is
// them, so that a change takes effect at once rather than at the next start-up.
//
// Editing settings.toml in the editor and saving it used to do nothing until
// the editor was restarted: UseSettings was called once, from main, and nothing
// called it again. The file is recognised by its path rather than by a
// remembered handle, so that *creating* it counts too — a project that had no
// settings file has no path remembered.
//
// Only autosave is re-applied. The theme is deliberately left alone: Options ▸
// Theme is the live way to change it and already writes the choice back into
// this file, and a -theme flag given on the command line is the more explicit
// statement of the two for the session it was given in.
func (a *App) reapplySettings(path string) {
	project, err := os.Getwd()
	if err != nil || !samePath(path, settings.Path(a.profile, project)) {
		return
	}

	loaded, err := settings.Load(a.profile, project)
	if err != nil {
		// Saved but not in force, which is a different thing from either
		// "saved" or "cannot save", and the only one of the three that leaves
		// the editor behaving unlike the file in front of you.
		a.Message("Saved, but not applied: " + err.Error())
		return
	}

	a.settingsPath = settings.Path(a.profile, project)
	a.SetAutosave(loaded.Autosave, loaded.AutosaveDelay)
	a.Message("Applied " + a.shortPath(path) + " — " + describeAutosave(loaded))
}

// describeAutosave says what a settings file has just asked for, in the few
// words a status bar has room for.
//
//	describeAutosave(settings.Settings{Autosave: true, AutosaveDelay: 2 * time.Second})
//	// "autosave on (2s)"
func describeAutosave(s settings.Settings) string {
	if !s.Autosave {
		return "autosave off"
	}
	return "autosave on (" + s.AutosaveDelay.String() + ")"
}

// samePath reports whether two paths name the same file, comparing them as
// absolute paths so that "./.turbo-go/settings.toml" and the absolute spelling
// of it are recognised as one file.
func samePath(one, other string) bool {
	oneAbs, err := filepath.Abs(one)
	if err != nil {
		return one == other
	}
	otherAbs, err := filepath.Abs(other)
	if err != nil {
		return one == other
	}
	return oneAbs == otherAbs
}

// rememberTheme writes the theme into the project's settings file, when the
// project has one.
//
// A project with no settings file is left without one: creating it is an
// explicit choice, and a theme picked to try it out should not put a new
// directory into someone's repository.
func (a *App) rememberTheme(name string) {
	if a.settingsPath == "" {
		return
	}
	if err := settings.SetTheme(a.settingsPath, name); err != nil {
		a.Message("Theme set for this session only: " + err.Error())
	}
}

// shortPath returns a path relative to the working directory when it is under
// it, because ".turbo-go/settings.toml" reads better in a status bar than the
// absolute path of the same file.
func (a *App) shortPath(path string) string {
	working, err := os.Getwd()
	if err != nil {
		return path
	}
	if relative, err := filepath.Rel(working, path); err == nil && !filepath.IsAbs(relative) {
		return relative
	}
	return path
}