1 module dprefhandler; 2 3 import std.stdio; 4 import std.conv: to; 5 import std.process: environment; 6 import std.file: mkdirRecurse, exists; 7 import std.exception: basicExceptionCtors; 8 9 /** 10 Collection of preferences, addressed by names, each containing actual value, 11 initial value and default value. 12 */ 13 class DPrefHandler 14 { 15 private: 16 // TODO switch to dchar and dstring 17 static const string ROW_PREFIX = ":::::"; 18 static const char EQSIGN = '='; 19 static const string CFG_FILE_NAME = "config.ini"; 20 version(Windows) 21 { 22 static const string PATH_SEP = "\\"; 23 } 24 version(Posix) 25 { 26 static const string PATH_SEP = "/"; 27 } 28 29 string _name, _configDirectoryPath; 30 DPref[string] prefs; 31 32 /** 33 Fill the value of class variable _configDirectoryPath with directory 34 which stores the config file. 35 */ 36 void generateConfigDirectoryPath() 37 { 38 string appDataDir = null; 39 version(Windows) 40 { 41 appDataDir = environment.get("APPDATA"); 42 } 43 version(linux) 44 { 45 appDataDir = "~/.local/share"; 46 } 47 version(OSX) 48 { 49 appDataDir = "~/Library/Application Support"; 50 } 51 if (appDataDir !is null) 52 { 53 _configDirectoryPath = appDataDir ~ PATH_SEP ~ name; 54 } 55 } 56 57 public: 58 /** 59 Constructs instance of DPrefHandler. 60 pName is later used as directory name in OS user preferences directory. 61 */ 62 this(string pName) 63 { 64 name = pName; 65 generateConfigDirectoryPath; 66 } 67 68 /** 69 Fill actual and initial values of all existing preferences, as well as 70 create new preferences with initial == existing == default values, from 71 config file, that is stored in OS user preferences directory. 72 73 Loading of values from file does not happen in constructor! 74 First developer creates the instance, then adds prefs with default values, 75 only after that pref values can be populated from file by calling this method. 76 If file has a pref that is no defined by developer, new pref is created automatically. 77 */ 78 string loadFromFile() 79 { 80 if (configDirectoryPath !is null) 81 { 82 mkdirRecurse(configDirectoryPath); 83 string fileFullPath = configDirectoryPath ~ PATH_SEP ~ CFG_FILE_NAME; 84 if (!exists(fileFullPath)) { 85 return null; 86 } 87 88 File file = File(fileFullPath, "r"); 89 90 char[] buf; 91 while (!file.eof) 92 { 93 char[] line = buf; 94 file.readln(line); 95 if (line.length > ROW_PREFIX.length + 1) 96 { 97 if (line[0..ROW_PREFIX.length] == ROW_PREFIX) 98 { 99 // Start of the config line 100 foreach (size_t i, char c; line[ROW_PREFIX.length..$]) 101 { 102 if (c == EQSIGN) 103 { 104 string key = line[ROW_PREFIX.length..ROW_PREFIX.length + i].idup; 105 string value = line[ROW_PREFIX.length + i + 1..$ - 1].idup; 106 try 107 { 108 // Key already exists 109 prefs[key].actualValue = value; 110 prefs[key].initialValue = value; 111 } 112 catch (core.exception.RangeError e) 113 { 114 // Key does not exist yet 115 prefs[key] = new DPref(key, value); 116 } 117 break; 118 } 119 } 120 } 121 } 122 else 123 { 124 // TODO 125 } 126 } 127 debug writeln(this); 128 return file.name; 129 } 130 return null; 131 } 132 133 /** 134 Save actual values of all preferences to config file, 135 that is stored in OS user preferences directory. 136 */ 137 string saveToFile() 138 { 139 if (configDirectoryPath !is null) 140 { 141 mkdirRecurse(configDirectoryPath); 142 File file = File(configDirectoryPath ~ PATH_SEP ~ CFG_FILE_NAME, "w+"); 143 foreach (key; prefs.byKey) 144 { 145 file.writeln(ROW_PREFIX ~ key ~ EQSIGN ~ prefs[key].actualValue); 146 } 147 return file.name; 148 } 149 return null; 150 } 151 152 /** 153 Create new preference or overwrite an existing one. 154 */ 155 DPrefHandler addPref(T)(string name, T defaultValue) 156 { 157 // TODO validate name, replace spaces with _, etc 158 prefs[name] = new DPref(name, to!string(defaultValue)); 159 return this; 160 } 161 162 /** 163 Get actual value of preference specified by provided name. 164 Throws DPrefException if preference with provided name does not exist. 165 */ 166 T getActualValue(T)(string propertyName) 167 { 168 try 169 { 170 return to!T(prefs[propertyName].actualValue); 171 } 172 catch (core.exception.RangeError e) 173 { 174 throw new DPrefException( 175 DPrefException.NO_PREF_FOUND ~ propertyName); 176 } 177 } 178 179 /** 180 Get iniital value of preference specified by provided name. 181 Throws DPrefException if preference with provided name does not exist. 182 */ 183 T getInitialValue(T)(string propertyName) 184 { 185 try 186 { 187 return to!T(prefs[propertyName].initialValue); 188 } 189 catch (core.exception.RangeError e) 190 { 191 throw new DPrefException( 192 DPrefException.NO_PREF_FOUND ~ propertyName); 193 } 194 } 195 196 /** 197 Get default value of preference specified by provided name. 198 Throws DPrefException if preference with provided name does not exist. 199 */ 200 T getDefaultValue(T)(string propertyName) 201 { 202 try 203 { 204 return to!T(prefs[propertyName].defaultValue); 205 } 206 catch (core.exception.RangeError e) 207 { 208 throw new DPrefException( 209 DPrefException.NO_PREF_FOUND ~ propertyName); 210 } 211 } 212 213 /** 214 Set provided value as actual value of preference specified by provided name. 215 Throws DPrefException if preference with provided name does not exist. 216 */ 217 void setActualValue(T)(string propertyName, T actualValue) 218 { 219 try 220 { 221 prefs[propertyName].actualValue = to!string(actualValue); 222 } 223 catch (core.exception.RangeError e) 224 { 225 throw new DPrefException( 226 DPrefException.NO_PREF_FOUND ~ propertyName); 227 } 228 } 229 230 /** 231 Revert actual value to initial one for the preference specified by provided name. 232 Throws DPrefException if preference with provided name does not exist. 233 */ 234 void revertActualToInitial(string propertyName) 235 { 236 try 237 { 238 prefs[propertyName].actualValue = prefs[propertyName].initialValue; 239 } 240 catch (core.exception.RangeError e) 241 { 242 throw new DPrefException( 243 DPrefException.NO_PREF_FOUND ~ propertyName); 244 } 245 } 246 247 /** 248 Revert actual values of all preferences to initial values. 249 */ 250 void revertAllActualToInitial() 251 { 252 foreach (key; prefs.byKey) 253 { 254 revertActualToInitial(key); 255 } 256 } 257 258 /** 259 Revert actual value to default one for the preference specified by provided name. 260 Throws DPrefException if preference with provided name does not exist. 261 */ 262 void revertActualToDefault(string propertyName) 263 { 264 try 265 { 266 prefs[propertyName].actualValue = prefs[propertyName].defaultValue; 267 } 268 catch (core.exception.RangeError e) 269 { 270 throw new DPrefException( 271 DPrefException.NO_PREF_FOUND ~ propertyName); 272 } 273 } 274 275 /** 276 Revert actual values of all preferences to default values. 277 */ 278 void revertAllActualToDefault() 279 { 280 foreach (key; prefs.byKey) 281 { 282 revertActualToDefault(key); 283 } 284 } 285 286 /// Get name of the preference handler (used as name of the config directory) 287 string name() const @property 288 { 289 return _name; 290 } 291 292 /// Set name of the preference handler (used as name of the config directory) 293 void name(string name) @property 294 { 295 // TODO generate default name if empty or null 296 _name = name; 297 } 298 299 /// Get path of the config directory 300 string configDirectoryPath() const @property 301 { 302 return _configDirectoryPath; 303 } 304 305 override string toString() const pure @safe 306 { 307 string result = _name ~ ":["; 308 foreach (key; prefs.byKey) 309 { 310 result ~= "{" ~ key ~ " : act(" ~ prefs[key].actualValue 311 ~ "), ini(" ~ prefs[key].initialValue 312 ~ "), def(" ~ prefs[key].defaultValue ~ ")}"; 313 } 314 result ~= "]"; 315 return result; 316 } 317 } 318 319 /** 320 Single preference contains: 321 - name 322 - actual value 323 - initial value 324 - default value 325 */ 326 private class DPref 327 { 328 private: 329 string name; 330 string defaultValue; 331 string initialValue; 332 string actualValue; 333 334 public: 335 this(string name, string defaultValue) 336 { 337 this.name = name; 338 this.defaultValue = defaultValue; 339 this.initialValue = defaultValue; 340 this.actualValue = defaultValue; 341 } 342 } 343 344 /** 345 Common Exception 346 */ 347 class DPrefException : Exception 348 { 349 private static const string NO_PREF_FOUND = "No preference found by key: "; 350 /// Constructor for an extended exception 351 mixin basicExceptionCtors; 352 }