package KFEditor import ( "bytes" "embed" "encoding/json" "errors" "io/fs" "log" "os" "os/exec" "path/filepath" "strings" "text/template" "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/widget" "g.r-io.lu/shynd/kemoforge/pkg/KFData/KFConfig" "g.r-io.lu/shynd/kemoforge/pkg/KFData/KFError" "g.r-io.lu/shynd/kemoforge/pkg/KFData/KFObjects/KFWidget/CalsWidgets" ) type KFInfo struct { Name string `json:"Name"` Path string `json:"Path"` OpenDate time.Time `json:"Last Opened"` } type KFProject struct { Info KFInfo Config *KFConfig.KFConfig } var ( //go:embed Templates/* Templates embed.FS ActiveProject *KFProject ) func UpdateProjectInfo(info KFInfo) error { homeDir, err := os.UserHomeDir() if err != nil { return err } kemoForgeDir := homeDir + "/Documents/KemoForge" projects, err := ReadProjectInfo() if err != nil { return err } _, err = os.Stat(info.Path) if os.IsNotExist(err) { // file does not exist so remove it from the projects.kf file for i, project := range projects { if project.Name == info.Name { projects = append(projects[:i], projects[i+1:]...) serializedProjects, err := json.Marshal(projects) if err != nil { return err } err = os.WriteFile(kemoForgeDir+"/projects.kf", serializedProjects, 0777) if err != nil { return err } return nil } } } projectFound := false for i, project := range projects { if project.Name == info.Name { projectFound = true projects[i] = info serializedProjects, err := json.Marshal(projects) if err != nil { return err } err = os.WriteFile(kemoForgeDir+"/projects.kf", serializedProjects, 0777) if err != nil { return err } return nil } } if !projectFound { projects = append(projects, info) serializedProjects, err := json.Marshal(projects) if err != nil { return err } err = os.WriteFile(kemoForgeDir+"/projects.kf", serializedProjects, 0777) if err != nil { return err } } return nil } // ReadProjectInfo reads the project info from the project file func ReadProjectInfo() ([]KFInfo, error) { homeDir, err := os.UserHomeDir() if err != nil { return nil, err } kemoForgeDir := homeDir + "/Documents/KemoForge" //Check if the KemoForge directory exists if _, err := os.Stat(kemoForgeDir); os.IsNotExist(err) { //if it doesn't exist, create it err = os.MkdirAll(kemoForgeDir, 0777) if err != nil { return nil, err } } //Check if the projects.kf file exists if _, err := os.Stat(kemoForgeDir + "/projects.kf"); os.IsNotExist(err) { //if it doesn't exist, create it err = os.WriteFile(kemoForgeDir+"/projects.kf", []byte("[]"), 0777) if err != nil { return nil, err } return nil, nil } //Read the file file, err := os.ReadFile(kemoForgeDir + "/projects.kf") if err != nil { return nil, err } //unmarshal the json into a slice of structs var projects []KFInfo err = json.Unmarshal(file, &projects) if err != nil { return nil, err } //Sort the projects by the last opened date for i := 0; i < len(projects); i++ { for j := 0; j < len(projects); j++ { if projects[i].OpenDate.After(projects[j].OpenDate) { temp := projects[i] projects[i] = projects[j] projects[j] = temp } } } //return the slice of structs return projects, nil } func OpenFromInfo(info KFInfo, window fyne.Window) error { path := info.Path //Check if the path even exists if _, err := os.Stat(path); os.IsNotExist(err) { newError := UpdateProjectInfo(info) if newError != nil { errors.Join(err, newError, KFError.NewErrProjectNotFound(info.Name)) } return err } //Check if the path ends in .KFProject if filepath.Ext(path) != ".KFProject" { //Check if the path is a directory and if the directory contains a .KFProject file if _, err := os.Stat(path + "/" + filepath.Base(path) + ".KFProject"); os.IsNotExist(err) { return KFError.NewErrProjectNotFound(info.Name) } else { //If it does, set the path to the directory path = path + "/" + filepath.Base(path) + ".KFProject" } } //Read the project file file, err := os.ReadFile(path) if err != nil { return err } //Deserialize the project project, err := Deserialize(file) if err != nil { return err } err = project.Load(window) if err != nil { return err } return nil } func Deserialize(file []byte) (project KFInfo, err error) { err = json.Unmarshal(file, &project) if err != nil { return KFInfo{}, err } return project, nil } // Load takes a deserialized project and loads it into the editor loading the scenes and functions as well func (p KFInfo) Load(window fyne.Window) error { Project := &KFProject{ Info: p, } //Walk the game directory of the project for the .KFConfig file //If it doesn't exist, return an error gameDir := filepath.Dir(p.Path) + "/data/" //Look for the first file ending in .KFConfig err := filepath.Walk(gameDir, func(path string, info os.FileInfo, err error) error { if filepath.Ext(path) == ".KFConfig" { file, err := os.Open(path) if err != nil { return err } err = Project.Config.Load(file) if err != nil { return err } } return nil }) err = UpdateProjectInfo(p) if err != nil { return err } ActiveProject = Project // window.SetContent(CreateSceneEditor(window)) // TODO return nil } func (p KFProject) Create(window fyne.Window) error { shouldDelete := false deletePath := "" defer func() { if shouldDelete && deletePath != "" { deletePath = filepath.Clean(deletePath) if fs.ValidPath(deletePath) { vbox := container.NewVBox( widget.NewLabel("An error occurred while creating the project."), widget.NewLabel("Would you like to delete the broken project?"), widget.NewLabel("Delete will remove: "+deletePath), widget.NewLabel("This action cannot be undone."), ) dialog.ShowCustomConfirm("Delete Broken Project?", "Yes", "No", vbox, func(b bool) { if b { err := os.RemoveAll(deletePath) if err != nil { dialog.ShowError(err, window) } } }, window) } } }() loadingChannel := make(chan struct{}) loading := CalsWidgets.NewLoading(loadingChannel, 100*time.Millisecond, 100) loading.SetProgress(0, "Creating Project: "+p.Config.Name) //Pop up a dialog with a progress bar and a label that says "Creating Project" progressDialog := dialog.NewCustomWithoutButtons("Creating Project", loading.Box, window) progressDialog.Show() defer progressDialog.Hide() homeDir, err := os.UserHomeDir() if err != nil { return err } kemoForgeDir := homeDir + "/Documents/KemoForge" projectsDir := fyne.CurrentApp().Preferences().StringWithFallback("projectDir", kemoForgeDir+"/projects") //First check if the project directory already exists projectDir := projectsDir + "/" + p.Config.Name projectDir = filepath.Clean(projectDir) if !fs.ValidPath(projectDir) { return errors.New("invalid path") } loading.SetProgress(10, "Checking if project already exists") _, err = os.Stat(projectDir) if os.IsNotExist(err) { err = os.MkdirAll(projectDir, os.ModePerm) if err != nil { return err } } else { return KFError.NewErrProjectAlreadyExists(p.Config.Name) } //Create the project directory loading.SetProgress(20, "Creating Project Directory") err = os.MkdirAll(projectDir, os.ModePerm) deletePath = projectDir if err != nil { shouldDelete = true return err } //Create the project file loading.SetProgress(30, "Creating Project File") projectFilePath := projectDir + "/" + p.Config.Name + ".KFProject" projectFilePath = filepath.Clean(projectFilePath) if !fs.ValidPath(projectFilePath) { shouldDelete = true return errors.New("invalid path") } err = os.WriteFile(projectFilePath, p.SerializeInfo(), os.ModePerm) if err != nil { shouldDelete = true return err } neededDirectories := []string{ "cmd/" + p.Config.Name, "data/assets/image", "data/assets/audio", "data/assets/video", "data/assets/other", "data/scenes", "internal/functions", "internal/layouts", "internal/widgets", } percentPerDir := 10 / len(neededDirectories) for _, dir := range neededDirectories { dirPath := projectDir + "/" + dir dirPath = filepath.Clean(dirPath) if !fs.ValidPath(dirPath) { shouldDelete = true return errors.New("invalid path") } loading.SetProgress(loading.GetProgress()+float64(percentPerDir), "Creating "+dir) err = os.MkdirAll(dirPath, os.ModePerm) if err != nil { shouldDelete = true return err } } loading.SetProgress(50, "Saving Local config") err = p.Config.Save(projectDir + "/data/Game.KFConfig") if err != nil { shouldDelete = true return err } // templateCombo is a struct that holds the template path, destination path, and data to be used in the template type templateCombo struct { templatePath string destinationPath string data interface{} } // neededFiles is a slice of templateCombos that holds all the files that need to be created var neededFiles []templateCombo //cmd/ProjectName/ProjectName.go mainGameData := struct { LocalFileSystem string LocalFunctions string LocalLayouts string LocalWidgets string }{ LocalFileSystem: p.Config.Name + "/data", LocalFunctions: p.Config.Name + "/internal/functions", LocalLayouts: p.Config.Name + "/internal/layouts", LocalWidgets: p.Config.Name + "/internal/widgets", } neededFiles = append(neededFiles, templateCombo{ templatePath: "Templates/MainGame/MainGame.got", destinationPath: projectDir + "/cmd/" + p.Config.Name + "/" + p.Config.Name + ".go", data: mainGameData, }) //data/FileSystem.go neededFiles = append(neededFiles, templateCombo{ templatePath: "Templates/FileLoader/FileLoader.got", destinationPath: projectDir + "/data/FileSystem.go", data: nil, }) //internal/functions/CustomFunctions.go neededFiles = append(neededFiles, templateCombo{ templatePath: "Templates/CustomFunction/CustomFunction.got", destinationPath: projectDir + "/internal/functions/CustomFunction.go", data: nil, }) //internal/layouts/CustomLayouts.go neededFiles = append(neededFiles, templateCombo{ templatePath: "Templates/CustomLayout/CustomLayout.got", destinationPath: projectDir + "/internal/layouts/CustomLayout.go", data: nil, }) //internal/widgets/CustomWidgets.go neededFiles = append(neededFiles, templateCombo{ templatePath: "Templates/CustomWidget/CustomWidget.got", destinationPath: projectDir + "/internal/widgets/CustomWidget.go", data: nil, }) //data/scenes/MainMenu.KFScene neededFiles = append(neededFiles, templateCombo{ templatePath: "Templates/Scenes/MainMenu.KFScene", destinationPath: projectDir + "/data/scenes/MainMenu.KFScene", data: nil, }) //data/scenes/NewGame.KFScene neededFiles = append(neededFiles, templateCombo{ templatePath: "Templates/Scenes/NewGame.KFScene", destinationPath: projectDir + "/data/scenes/NewGame.KFScene", data: nil, }) percentPerFile := 30 / len(neededFiles) for _, file := range neededFiles { pathWithoutProject := strings.TrimPrefix(file.destinationPath, projectDir) loading.SetProgress(loading.GetProgress()+float64(percentPerFile), "Creating "+pathWithoutProject) err = MakeFromTemplate(file.templatePath, file.destinationPath, file.data) if err != nil { shouldDelete = true return err } } //Initialize the go mod file by running go mod init with os/exec loading.SetProgress(80, "Initializing go mod file") // Initialize the go mod var stderr bytes.Buffer cmd := exec.Command("go", "mod", "init", p.Config.Name) cmd.Stderr = &stderr cmd.Dir = projectDir err = cmd.Run() if err != nil { shouldDelete = true log.Printf("Error initializing go mod file: %v", err) log.Printf("Stderr: %s", stderr.String()) return errors.New("error initializing go mod file") } loading.SetProgress(90, "Installing fyne") cmd = exec.Command("go", "get", "fyne.io/fyne/v2@latest") cmd.Stderr = &stderr cmd.Dir = projectDir err = cmd.Run() if err != nil { log.Printf("Error installing fyne: %v", err) log.Printf("Stderr: %s", stderr.String()) return errors.New("error installing fyne") } loading.SetProgress(95, "Installing KemoForge") // cmd = exec.Command("go", "get", "g.r-io.lu/shynd/kemoforge@latest") // cmd.Stderr = &stderr // cmd.Dir = projectDir // err = cmd.Run() // if err != nil { // log.Printf("Error installing KemoForge: %v", err) // log.Printf("Stderr: %s", stderr.String()) // return errors.New("error installing KemoForge") // } //Run go mod tidy loading.SetProgress(97, "Running go mod tidy") cmd = exec.Command("go", "mod", "tidy") cmd.Stderr = &stderr cmd.Dir = projectDir err = cmd.Run() if err != nil { log.Printf("Error running go mod tidy: %v", err) log.Printf("Stderr: %s", stderr.String()) return errors.New("error running go mod tidy") } loading.SetProgress(100, "Project Created") time.Sleep(1 * time.Second) return nil } func (p KFProject) SerializeInfo() []byte { //Marshal the project to JSON serializedProject, err := json.MarshalIndent(p.Info, "", " ") if err != nil { return nil } return serializedProject } func MakeFromTemplate(templatePath, destinationPath string, data interface{}) error { destinationPath = filepath.Clean(destinationPath) if !fs.ValidPath(destinationPath) { return errors.New("invalid path") } _, err := fs.Stat(Templates, templatePath) if err != nil { return err } destinationFile, err := os.Create(destinationPath) defer destinationFile.Close() if err != nil { return err } t, err := template.ParseFS(Templates, templatePath) if err != nil { return err } err = t.Execute(destinationFile, data) if err != nil { return err } return nil }