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