diff --git a/config.go b/config.go new file mode 100644 index 0000000..0db21b1 --- /dev/null +++ b/config.go @@ -0,0 +1,150 @@ +package chatpb + +// functions to import and export the protobuf +// data to and from config files + +import ( + "errors" + "os" + "path/filepath" + + "go.wit.com/lib/protobuf/bugpb" + "go.wit.com/log" +) + +// write to ~/.config/gemini/ unless ENV{GEMINI_HOME} is set +func (all *Chats) ConfigSave() error { + if os.Getenv("GEMINI_HOME") == "" { + homeDir, _ := os.UserHomeDir() + fullpath := filepath.Join(homeDir, ".config/gemini") + os.Setenv("GEMINI_HOME", fullpath) + } + if all == nil { + log.Warn("chatpb all == nil") + return errors.New("chatpb.ConfigSave() all == nil") + } + + data, err := all.Marshal() + if err != nil { + log.Info("chatpb proto.Marshal() failed len", len(data), err) + // often this is because strings have invalid UTF-8. This should probably be fixed in the protobuf code + if err := all.tryValidate(); err != nil { + return err + } else { + // re-attempt Marshal() here + data, err = all.Marshal() + if err == nil { + // validate & sanitize strings worked + log.Info("chatpb.ConfigSave() pb.Marshal() worked len", len(all.Chats), "chats") + configWrite("gemini.pb", data) + return nil + } + } + return err + } + if err := configWrite("gemini.pb", data); err != nil { + log.Infof("chatpb.ConfigSave() failed len(Chats)=%d bytes=%d", len(all.Chats), len(data)) + return err + } + configWrite("gemini.text", []byte(all.FormatTEXT())) + log.Infof("chatpb.ConfigSave() worked len(Chats)=%d bytes=%d", len(all.Chats), len(data)) + return nil +} + +func (all *Chats) tryValidate() error { + + err := bugpb.ValidateProtoUTF8(all) + if err != nil { + log.Printf("Protobuf UTF-8 validation failed: %v\n", err) + } + if err := bugpb.SanitizeProtoUTF8(all); err != nil { + log.Warn("Sanitation failed:", err) + // log.Fatalf("Sanitization failed: %v", err) + return err + } + return nil +} + +// load the gemini.pb file. I shouldn't really matter if this +// fails. the file should be autogenerated. This is used +// locally just for speed +func (all *Chats) ConfigLoad() error { + if os.Getenv("GEMINI_HOME") == "" { + homeDir, _ := os.UserHomeDir() + fullpath := filepath.Join(homeDir, ".config/gemini") + os.Setenv("GEMINI_HOME", fullpath) + } + var data []byte + var err error + + cfgname := filepath.Join(os.Getenv("GEMINI_HOME"), "gemini.pb") + if data, err = loadFile(cfgname); err != nil { + // something went wrong loading the file + // all.sampleConfig() // causes nil panic + return err + } + // this means the gemini.pb file exists and was read + if len(data) == 0 { + // todo: add default data here since it's blank? // might cause nil panic? + return errors.New("chatpb.ConfigLoad() gemini.pb is empty") + } + err = all.Unmarshal(data) + test := NewChats() + if test.Uuid != all.Uuid { + log.Warn("uuids do not match", test.Uuid, all.Uuid) + deleteProtobufFile(cfgname) + } + if test.Version != all.Version { + log.Warn("versions do not match", test.Version, all.Version) + deleteProtobufFile(cfgname) + } + log.Info(cfgname, "protobuf versions and uuid match", all.Uuid, all.Version) + return err +} + +func deleteProtobufFile(filename string) { + log.Warn("The protobuf file format has changed for", filename) + log.Warn("Deleting old file:", filename) + log.Warn("This file will be recreated on the next run.") + err := os.Remove(filename) + if err != nil { + log.Warn("failed to remove old protobuf file", "err", err) + } +} + +func loadFile(fullname string) ([]byte, error) { + data, err := os.ReadFile(fullname) + if errors.Is(err, os.ErrNotExist) { + // if file does not exist, just return nil. this + // will cause ConfigLoad() to try the next config file like "gemini.text" + // because the user might want to edit the .config by hand + return nil, nil + } + if err != nil { + // log.Info("open config file :", err) + return nil, err + } + return data, nil +} + +func configWrite(filename string, data []byte) error { + fullname := filepath.Join(os.Getenv("GEMINI_HOME"), filename) + + cfgfile, err := os.OpenFile(fullname, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + defer cfgfile.Close() + if err != nil { + log.Warn("open config file :", err) + return err + } + if filename == "gemini.text" { + // add header + cfgfile.Write([]byte("# this file is automatically re-generated from gemini.pb, however,\n")) + cfgfile.Write([]byte("# if you want to edit it by hand, you can:\n")) + cfgfile.Write([]byte("# stop gemini; remove gemini.pb; edit gemini.text; start gemini\n")) + cfgfile.Write([]byte("# this will cause the default behavior to fallback to parsing this file for the config\n")) + cfgfile.Write([]byte("\n")) + cfgfile.Write([]byte("# this file is intended to be used to customize settings on what\n")) + } + cfgfile.Write(data) + return nil +} diff --git a/example.go b/example.go new file mode 100644 index 0000000..d22f39c --- /dev/null +++ b/example.go @@ -0,0 +1,22 @@ +package chatpb + +import ( + "go.wit.com/log" +) + +func ExampleChat() *Chats { + conversation := NewChats() + + t := conversation.AddTable() + t.AddRow([]string{"apple", "pear"}) + + conversation.AddGeminiComment("funny") + conversation.AddUserComment("yes") + + conversation.AddGeminiComment("I like astronomy") + + dump := conversation.FormatTEXT() + + log.Println(dump) + return conversation +}