@@ -143,6 +143,118 @@ bool UserConfig::setActiveGamepadBindings(std::string name) {
143143 return true ;
144144}
145145
146+ std::optional<BindingMapJson> UserConfig::loadProfile (std::string_view name) const {
147+ if (!isValidProfileName (name)) return std::nullopt ;
148+ const fs::path p = bindingsDir_ / (std::string (name) + " .json" );
149+ auto text = slurp (p);
150+ if (text.empty ()) return std::nullopt ;
151+ return bindingMapFromJson (text);
152+ }
153+
154+ bool UserConfig::isValidProfileName (std::string_view name) {
155+ if (name.empty ()) return false ;
156+ if (name == " config" ) return false ; // would collide with config.json
157+ for (char c : name) {
158+ const bool ok = (c >= ' a' && c <= ' z' )
159+ || (c >= ' A' && c <= ' Z' )
160+ || (c >= ' 0' && c <= ' 9' )
161+ || c == ' _' || c == ' -' ;
162+ if (!ok) return false ;
163+ }
164+ return true ;
165+ }
166+
167+ bool UserConfig::saveProfile (std::string name, BindingMapJson bindings) {
168+ if (!started_.load ()) return false ;
169+ if (!isValidProfileName (name)) return false ;
170+ bindings.schemaVersion = 1 ;
171+ bindings.name = name;
172+ const fs::path target = bindingsDir_ / (name + " .json" );
173+ if (!atomicWrite (target, bindingMapToJson (bindings))) {
174+ std::fprintf (stderr, " [user-config] failed to write %s\n " ,
175+ target.string ().c_str ());
176+ return false ;
177+ }
178+ reloadFromDisk ();
179+ if (onReload_) onReload_ ();
180+ return true ;
181+ }
182+
183+ bool UserConfig::renameProfile (std::string oldName, std::string newName) {
184+ if (!started_.load ()) return false ;
185+ if (!isValidProfileName (oldName) || !isValidProfileName (newName)) return false ;
186+ if (oldName == newName) return true ;
187+ const fs::path src = bindingsDir_ / (oldName + " .json" );
188+ const fs::path dst = bindingsDir_ / (newName + " .json" );
189+ std::error_code ec;
190+ if (!fs::exists (src, ec)) return false ;
191+ if (fs::exists (dst, ec)) return false ; // refuse to clobber
192+
193+ // If the source carries an embedded `name` field, rewrite it so the
194+ // file's content matches its new filename (cosmetic — the loader uses
195+ // the filename, not the field). Keeps hand-inspection sane.
196+ if (auto text = slurp (src); !text.empty ()) {
197+ if (auto parsed = bindingMapFromJson (text)) {
198+ parsed->name = newName;
199+ const fs::path tmpRename = src.string () + " .renaming" ;
200+ if (!atomicWrite (tmpRename, bindingMapToJson (*parsed))) return false ;
201+ fs::rename (tmpRename, src, ec);
202+ if (ec) { fs::remove (tmpRename, ec); return false ; }
203+ }
204+ }
205+
206+ fs::rename (src, dst, ec);
207+ if (ec) return false ;
208+
209+ // Repoint active profile references in config.json if needed.
210+ bool rewrote = false ;
211+ UserConfigJson cfg;
212+ cfg.schemaVersion = 1 ;
213+ {
214+ std::lock_guard<std::mutex> lock (mu_);
215+ cfg.activeKeyboardBindings = current_.activeKeyboardBindings ;
216+ cfg.activeGamepadBindings = current_.activeGamepadBindings ;
217+ cfg.defaultZoom = current_.defaultZoom ;
218+ if (cfg.activeKeyboardBindings == oldName) {
219+ cfg.activeKeyboardBindings = newName;
220+ rewrote = true ;
221+ }
222+ if (cfg.activeGamepadBindings == oldName) {
223+ cfg.activeGamepadBindings = newName;
224+ rewrote = true ;
225+ }
226+ }
227+ if (rewrote) {
228+ if (!atomicWrite (configFile_, userConfigToJson (cfg))) {
229+ std::fprintf (stderr, " [user-config] failed to update %s after rename\n " ,
230+ configFile_.string ().c_str ());
231+ return false ;
232+ }
233+ }
234+
235+ reloadFromDisk ();
236+ if (onReload_) onReload_ ();
237+ return true ;
238+ }
239+
240+ bool UserConfig::deleteProfile (std::string name) {
241+ if (!started_.load ()) return false ;
242+ if (!isValidProfileName (name)) return false ;
243+ {
244+ std::lock_guard<std::mutex> lock (mu_);
245+ if (name == current_.activeKeyboardBindings ) return false ;
246+ if (name == current_.activeGamepadBindings ) return false ;
247+ }
248+ const fs::path target = bindingsDir_ / (name + " .json" );
249+ std::error_code ec;
250+ if (!fs::exists (target, ec)) return false ;
251+ fs::remove (target, ec);
252+ if (ec) return false ;
253+ reloadFromDisk ();
254+ if (onReload_) onReload_ ();
255+ return true ;
256+ }
257+
146258void UserConfig::reloadFromDisk () {
147259 UserConfigDto next;
148260 {
0 commit comments