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