diff options
| author | 2020-08-04 20:42:39 +0200 | |
|---|---|---|
| committer | 2020-08-04 14:42:39 -0400 | |
| commit | 75a3442f9ff38222606a1e24753d4a57da1e8c0a (patch) | |
| tree | 5b0e12fb055d15fcb31b0fd07ae4cf9512e0758e /enigma/src/main/java/cuchaz | |
| parent | Revamp About dialog (diff) | |
| download | enigma-fork-75a3442f9ff38222606a1e24753d4a57da1e8c0a.tar.gz enigma-fork-75a3442f9ff38222606a1e24753d4a57da1e8c0a.tar.xz enigma-fork-75a3442f9ff38222606a1e24753d4a57da1e8c0a.zip | |
Configuration stuff (#301)
* Begin writing new config system
* Make config work
* Save window size and position
* Add editor font chooser
* Use *.ini for windows and mac instead of *rc
* Allow for changing language without having to restart the program
* Save selected directory in file dialogs
* Make dialog visible after moving it to the correct position
* Don't change theme on the fly since it's broken
* Remove unused gui parameter
* Use xdg-open to open URLs on Linux since Desktop.browse doesn't work, at least not on my PC
* Fix default proposed highlight color
* Multi font selection dialog thingy
* Remember network options
* Make font selection dialog actually work
* Collapse general actions ("OK", "Cancel", ..) into one translation
* Localize font dialog
* Use enum name when saving colors for consistency with currently selected theme
* Save size of split panes
* Import old config
* Add test & fix some parts of the config serializer
* TranslationChangeListener/TranslationUtil -> LanguageChangeListener/LanguageUtil
Diffstat (limited to 'enigma/src/main/java/cuchaz')
6 files changed, 667 insertions, 0 deletions
diff --git a/enigma/src/main/java/cuchaz/enigma/config/ConfigContainer.java b/enigma/src/main/java/cuchaz/enigma/config/ConfigContainer.java new file mode 100644 index 0000000..cb9cbc2 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/config/ConfigContainer.java | |||
| @@ -0,0 +1,97 @@ | |||
| 1 | package cuchaz.enigma.config; | ||
| 2 | |||
| 3 | import java.io.IOException; | ||
| 4 | import java.nio.charset.StandardCharsets; | ||
| 5 | import java.nio.file.Files; | ||
| 6 | import java.nio.file.Path; | ||
| 7 | import java.util.Deque; | ||
| 8 | import java.util.LinkedList; | ||
| 9 | |||
| 10 | public class ConfigContainer { | ||
| 11 | |||
| 12 | private Path configPath; | ||
| 13 | private boolean existsOnDisk; | ||
| 14 | |||
| 15 | private final ConfigSection root = new ConfigSection(); | ||
| 16 | |||
| 17 | public ConfigSection data() { | ||
| 18 | return this.root; | ||
| 19 | } | ||
| 20 | |||
| 21 | public void save() { | ||
| 22 | if (this.configPath == null) throw new IllegalStateException("File has no config path set!"); | ||
| 23 | try { | ||
| 24 | Files.createDirectories(this.configPath.getParent()); | ||
| 25 | Files.write(this.configPath, this.serialize().getBytes(StandardCharsets.UTF_8)); | ||
| 26 | this.existsOnDisk = true; | ||
| 27 | } catch (IOException e) { | ||
| 28 | e.printStackTrace(); | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | public void saveAs(Path path) { | ||
| 33 | this.configPath = path; | ||
| 34 | this.save(); | ||
| 35 | } | ||
| 36 | |||
| 37 | public boolean existsOnDisk() { | ||
| 38 | return this.existsOnDisk; | ||
| 39 | } | ||
| 40 | |||
| 41 | public String serialize() { | ||
| 42 | return ConfigSerializer.structureToString(this.root); | ||
| 43 | } | ||
| 44 | |||
| 45 | public static ConfigContainer create() { | ||
| 46 | return new ConfigContainer(); | ||
| 47 | } | ||
| 48 | |||
| 49 | public static ConfigContainer getOrCreate(String name) { | ||
| 50 | return ConfigContainer.getOrCreate(ConfigPaths.getConfigFilePath(name)); | ||
| 51 | } | ||
| 52 | |||
| 53 | public static ConfigContainer getOrCreate(Path path) { | ||
| 54 | ConfigContainer cc = null; | ||
| 55 | try { | ||
| 56 | if (Files.exists(path)) { | ||
| 57 | String s = String.join("\n", Files.readAllLines(path)); | ||
| 58 | cc = ConfigContainer.parse(s); | ||
| 59 | cc.existsOnDisk = true; | ||
| 60 | } | ||
| 61 | } catch (IOException e) { | ||
| 62 | e.printStackTrace(); | ||
| 63 | } | ||
| 64 | |||
| 65 | if (cc == null) { | ||
| 66 | cc = ConfigContainer.create(); | ||
| 67 | } | ||
| 68 | |||
| 69 | cc.configPath = path; | ||
| 70 | return cc; | ||
| 71 | } | ||
| 72 | |||
| 73 | public static ConfigContainer parse(String source) { | ||
| 74 | ConfigContainer cc = ConfigContainer.create(); | ||
| 75 | Deque<ConfigSection> stack = new LinkedList<>(); | ||
| 76 | stack.push(cc.root); | ||
| 77 | ConfigSerializer.parse(source, new ConfigStructureVisitor() { | ||
| 78 | @Override | ||
| 79 | public void visitKeyValue(String key, String value) { | ||
| 80 | stack.peekLast().setString(key, value); | ||
| 81 | } | ||
| 82 | |||
| 83 | @Override | ||
| 84 | public void visitSection(String section) { | ||
| 85 | stack.add(stack.peekLast().section(section)); | ||
| 86 | } | ||
| 87 | |||
| 88 | @Override | ||
| 89 | public void jumpToRootSection() { | ||
| 90 | stack.clear(); | ||
| 91 | stack.push(cc.root); | ||
| 92 | } | ||
| 93 | }); | ||
| 94 | return cc; | ||
| 95 | } | ||
| 96 | |||
| 97 | } | ||
diff --git a/enigma/src/main/java/cuchaz/enigma/config/ConfigPaths.java b/enigma/src/main/java/cuchaz/enigma/config/ConfigPaths.java new file mode 100644 index 0000000..6e668f8 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/config/ConfigPaths.java | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | package cuchaz.enigma.config; | ||
| 2 | |||
| 3 | import java.nio.file.Path; | ||
| 4 | import java.nio.file.Paths; | ||
| 5 | |||
| 6 | import cuchaz.enigma.utils.Os; | ||
| 7 | |||
| 8 | public class ConfigPaths { | ||
| 9 | |||
| 10 | public static Path getConfigFilePath(String name) { | ||
| 11 | String fileName = Os.getOs() == Os.LINUX ? String.format("%src", name) : String.format("%s.ini", name); | ||
| 12 | return getConfigPathRoot().resolve(fileName); | ||
| 13 | } | ||
| 14 | |||
| 15 | public static Path getConfigPathRoot() { | ||
| 16 | switch (Os.getOs()) { | ||
| 17 | case LINUX: | ||
| 18 | String configHome = System.getenv("XDG_CONFIG_HOME"); | ||
| 19 | if (configHome == null) { | ||
| 20 | return getUserHomeUnix().resolve(".config"); | ||
| 21 | } | ||
| 22 | return Paths.get(configHome); | ||
| 23 | case MAC: | ||
| 24 | return getUserHomeUnix().resolve("Library").resolve("Preferences"); | ||
| 25 | case WINDOWS: | ||
| 26 | return Paths.get(System.getenv("LOCALAPPDATA")); | ||
| 27 | default: | ||
| 28 | return Paths.get(System.getProperty("user.dir")); | ||
| 29 | } | ||
| 30 | } | ||
| 31 | |||
| 32 | private static Path getUserHomeUnix() { | ||
| 33 | String userHome = System.getenv("HOME"); | ||
| 34 | if (userHome == null) { | ||
| 35 | userHome = System.getProperty("user.dir"); | ||
| 36 | } | ||
| 37 | return Paths.get(userHome); | ||
| 38 | } | ||
| 39 | |||
| 40 | } | ||
diff --git a/enigma/src/main/java/cuchaz/enigma/config/ConfigSection.java b/enigma/src/main/java/cuchaz/enigma/config/ConfigSection.java new file mode 100644 index 0000000..3e7bf6d --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/config/ConfigSection.java | |||
| @@ -0,0 +1,183 @@ | |||
| 1 | package cuchaz.enigma.config; | ||
| 2 | |||
| 3 | import java.util.*; | ||
| 4 | import java.util.function.Function; | ||
| 5 | |||
| 6 | public class ConfigSection { | ||
| 7 | |||
| 8 | private final Map<String, String> values; | ||
| 9 | private final Map<String, ConfigSection> sections; | ||
| 10 | |||
| 11 | private ConfigSection(Map<String, String> values, Map<String, ConfigSection> sections) { | ||
| 12 | this.values = values; | ||
| 13 | this.sections = sections; | ||
| 14 | } | ||
| 15 | |||
| 16 | public ConfigSection() { | ||
| 17 | this(new HashMap<>(), new HashMap<>()); | ||
| 18 | } | ||
| 19 | |||
| 20 | public ConfigSection section(String name) { | ||
| 21 | return this.sections.computeIfAbsent(name, _s -> new ConfigSection()); | ||
| 22 | } | ||
| 23 | |||
| 24 | public Map<String, String> values() { | ||
| 25 | return Collections.unmodifiableMap(this.values); | ||
| 26 | } | ||
| 27 | |||
| 28 | public Map<String, ConfigSection> sections() { | ||
| 29 | return Collections.unmodifiableMap(this.sections); | ||
| 30 | } | ||
| 31 | |||
| 32 | public boolean remove(String key) { | ||
| 33 | return this.values.remove(key) != null; | ||
| 34 | } | ||
| 35 | |||
| 36 | public boolean removeSection(String key) { | ||
| 37 | return this.sections.remove(key) != null; | ||
| 38 | } | ||
| 39 | |||
| 40 | public Optional<String> getString(String key) { | ||
| 41 | return Optional.ofNullable(this.values.get(key)); | ||
| 42 | } | ||
| 43 | |||
| 44 | public void setString(String key, String value) { | ||
| 45 | this.values.put(key, value); | ||
| 46 | } | ||
| 47 | |||
| 48 | public String setIfAbsentString(String key, String value) { | ||
| 49 | this.values.putIfAbsent(key, value); | ||
| 50 | return this.values.get(key); | ||
| 51 | } | ||
| 52 | |||
| 53 | public Optional<Boolean> getBool(String key) { | ||
| 54 | return ConfigSerializer.parseBool(this.values.get(key)); | ||
| 55 | } | ||
| 56 | |||
| 57 | public boolean setIfAbsentBool(String key, boolean value) { | ||
| 58 | return this.getBool(key).orElseGet(() -> { | ||
| 59 | this.setBool(key, value); | ||
| 60 | return value; | ||
| 61 | }); | ||
| 62 | } | ||
| 63 | |||
| 64 | public void setBool(String key, boolean value) { | ||
| 65 | this.values.put(key, Boolean.toString(value)); | ||
| 66 | } | ||
| 67 | |||
| 68 | public OptionalInt getInt(String key) { | ||
| 69 | return ConfigSerializer.parseInt(this.values.get(key)); | ||
| 70 | } | ||
| 71 | |||
| 72 | public void setInt(String key, int value) { | ||
| 73 | this.values.put(key, Integer.toString(value)); | ||
| 74 | } | ||
| 75 | |||
| 76 | public int setIfAbsentInt(String key, int value) { | ||
| 77 | return this.getInt(key).orElseGet(() -> { | ||
| 78 | this.setInt(key, value); | ||
| 79 | return value; | ||
| 80 | }); | ||
| 81 | } | ||
| 82 | |||
| 83 | public OptionalDouble getDouble(String key) { | ||
| 84 | return ConfigSerializer.parseDouble(this.values.get(key)); | ||
| 85 | } | ||
| 86 | |||
| 87 | public void setDouble(String key, double value) { | ||
| 88 | this.values.put(key, Double.toString(value)); | ||
| 89 | } | ||
| 90 | |||
| 91 | public double setIfAbsentDouble(String key, double value) { | ||
| 92 | return this.getDouble(key).orElseGet(() -> { | ||
| 93 | this.setDouble(key, value); | ||
| 94 | return value; | ||
| 95 | }); | ||
| 96 | } | ||
| 97 | |||
| 98 | public OptionalInt getRgbColor(String key) { | ||
| 99 | return ConfigSerializer.parseRgbColor(this.values.get(key)); | ||
| 100 | } | ||
| 101 | |||
| 102 | public void setRgbColor(String key, int value) { | ||
| 103 | this.values.put(key, ConfigSerializer.rgbColorToString(value)); | ||
| 104 | } | ||
| 105 | |||
| 106 | public int setIfAbsentRgbColor(String key, int value) { | ||
| 107 | return this.getRgbColor(key).orElseGet(() -> { | ||
| 108 | this.setRgbColor(key, value); | ||
| 109 | return value; | ||
| 110 | }); | ||
| 111 | } | ||
| 112 | |||
| 113 | public Optional<String[]> getArray(String key) { | ||
| 114 | return ConfigSerializer.parseArray(this.values.get(key)); | ||
| 115 | } | ||
| 116 | |||
| 117 | public void setArray(String key, String[] value) { | ||
| 118 | this.values.put(key, ConfigSerializer.arrayToString(value)); | ||
| 119 | } | ||
| 120 | |||
| 121 | public String[] setIfAbsentArray(String key, String[] value) { | ||
| 122 | return this.getArray(key).orElseGet(() -> { | ||
| 123 | this.setArray(key, value); | ||
| 124 | return value; | ||
| 125 | }); | ||
| 126 | } | ||
| 127 | |||
| 128 | public Optional<int[]> getIntArray(String key) { | ||
| 129 | return this.getArray(key).map(arr -> Arrays.stream(arr).mapToInt(s -> ConfigSerializer.parseInt(s).orElse(0)).toArray()); | ||
| 130 | } | ||
| 131 | |||
| 132 | public void setIntArray(String key, int[] value) { | ||
| 133 | this.setArray(key, Arrays.stream(value).mapToObj(Integer::toString).toArray(String[]::new)); | ||
| 134 | } | ||
| 135 | |||
| 136 | public int[] setIfAbsentIntArray(String key, int[] value) { | ||
| 137 | return this.getIntArray(key).orElseGet(() -> { | ||
| 138 | this.setIntArray(key, value); | ||
| 139 | return value; | ||
| 140 | }); | ||
| 141 | } | ||
| 142 | |||
| 143 | public <T extends Enum<T>> Optional<T> getEnum(Function<String, T> byName, String key) { | ||
| 144 | return ConfigSerializer.parseEnum(byName, this.values.get(key)); | ||
| 145 | } | ||
| 146 | |||
| 147 | public <T extends Enum<T>> void setEnum(String key, T value) { | ||
| 148 | this.values.put(key, value.name()); | ||
| 149 | } | ||
| 150 | |||
| 151 | public <T extends Enum<T>> T setIfAbsentEnum(Function<String, T> byName, String key, T value) { | ||
| 152 | return this.getEnum(byName, key).orElseGet(() -> { | ||
| 153 | this.setEnum(key, value); | ||
| 154 | return value; | ||
| 155 | }); | ||
| 156 | } | ||
| 157 | |||
| 158 | public ConfigSection copy() { | ||
| 159 | Map<String, ConfigSection> sections = new HashMap<>(this.sections); | ||
| 160 | sections.replaceAll((k, v) -> v.copy()); | ||
| 161 | return new ConfigSection(new HashMap<>(this.values), sections); | ||
| 162 | } | ||
| 163 | |||
| 164 | @Override | ||
| 165 | public boolean equals(Object o) { | ||
| 166 | if (this == o) return true; | ||
| 167 | if (!(o instanceof ConfigSection)) return false; | ||
| 168 | ConfigSection that = (ConfigSection) o; | ||
| 169 | return values.equals(that.values) && | ||
| 170 | sections.equals(that.sections); | ||
| 171 | } | ||
| 172 | |||
| 173 | @Override | ||
| 174 | public int hashCode() { | ||
| 175 | return Objects.hash(values, sections); | ||
| 176 | } | ||
| 177 | |||
| 178 | @Override | ||
| 179 | public String toString() { | ||
| 180 | return String.format("ConfigSection { values: %s, sections: %s }", values, sections); | ||
| 181 | } | ||
| 182 | |||
| 183 | } | ||
diff --git a/enigma/src/main/java/cuchaz/enigma/config/ConfigSerializer.java b/enigma/src/main/java/cuchaz/enigma/config/ConfigSerializer.java new file mode 100644 index 0000000..dccb585 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/config/ConfigSerializer.java | |||
| @@ -0,0 +1,303 @@ | |||
| 1 | package cuchaz.enigma.config; | ||
| 2 | |||
| 3 | import java.util.*; | ||
| 4 | import java.util.Map.Entry; | ||
| 5 | import java.util.function.Function; | ||
| 6 | import java.util.regex.Pattern; | ||
| 7 | import java.util.stream.Collectors; | ||
| 8 | |||
| 9 | public final class ConfigSerializer { | ||
| 10 | |||
| 11 | private static final Pattern FULL_RGB_COLOR = Pattern.compile("#[0-9A-Fa-f]{6}"); | ||
| 12 | private static final Pattern MIN_RGB_COLOR = Pattern.compile("#[0-9A-Fa-f]{3}"); | ||
| 13 | |||
| 14 | private static final int UNEXPECTED_TOKEN = -1; | ||
| 15 | private static final int NO_MATCH = -2; | ||
| 16 | |||
| 17 | public static void parse(String v, ConfigStructureVisitor visitor) { | ||
| 18 | String[] lines = v.split("\n"); | ||
| 19 | |||
| 20 | // join escaped newlines | ||
| 21 | int len = lines.length; | ||
| 22 | for (int i = len - 2; i >= 0; i--) { | ||
| 23 | if (lines[i].endsWith("\\")) { | ||
| 24 | lines[i] = String.format("%s\n%s", lines[i], lines[i + 1]); | ||
| 25 | len -= 1; | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | // parse for real | ||
| 30 | for (int i = 0; i < len; i++) { | ||
| 31 | String line = lines[i]; | ||
| 32 | |||
| 33 | // skip empty lines and comment lines | ||
| 34 | if (line.trim().isEmpty() || line.trim().startsWith(";")) continue; | ||
| 35 | |||
| 36 | int r; | ||
| 37 | boolean fail = (r = parseSectionLine(line, 0, visitor)) == NO_MATCH && | ||
| 38 | (r = parseKeyValue(line, 0, visitor)) == NO_MATCH; | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | private static int parseSectionLine(String v, int idx, ConfigStructureVisitor visitor) { | ||
| 43 | if (v.startsWith("[")) { | ||
| 44 | List<String> path = new ArrayList<>(); | ||
| 45 | while (idx < v.length() && v.charAt(idx) == '[') { | ||
| 46 | idx = parseSection(v, idx, path); | ||
| 47 | if (idx == UNEXPECTED_TOKEN) return UNEXPECTED_TOKEN; | ||
| 48 | } | ||
| 49 | |||
| 50 | if (!path.isEmpty()) { | ||
| 51 | visitor.jumpToRootSection(); | ||
| 52 | for (String s : path) { | ||
| 53 | visitor.visitSection(s); | ||
| 54 | } | ||
| 55 | } | ||
| 56 | |||
| 57 | return v.length(); | ||
| 58 | } else { | ||
| 59 | return NO_MATCH; | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | private static int parseSection(String v, int idx, List<String> path) { | ||
| 64 | idx += 1; // skip leading [ | ||
| 65 | StringBuilder sb = new StringBuilder(); | ||
| 66 | while (idx < v.length()) { | ||
| 67 | int nextCloseBracket = v.indexOf(']', idx); | ||
| 68 | int nextEscape = v.indexOf('\\', idx); | ||
| 69 | int next = optMin(nextCloseBracket, nextEscape); | ||
| 70 | if (next == -1) { | ||
| 71 | // unexpected | ||
| 72 | return UNEXPECTED_TOKEN; | ||
| 73 | } else if (next == nextCloseBracket) { | ||
| 74 | sb.append(v, idx, nextCloseBracket); | ||
| 75 | path.add(sb.toString()); | ||
| 76 | return nextCloseBracket + 1; | ||
| 77 | } else if (next == nextEscape) { | ||
| 78 | sb.append(v, idx, nextEscape); | ||
| 79 | idx = parseEscape(v, nextEscape, sb); | ||
| 80 | } | ||
| 81 | } | ||
| 82 | return idx; | ||
| 83 | } | ||
| 84 | |||
| 85 | private static int parseKeyValue(String v, int idx, ConfigStructureVisitor visitor) { | ||
| 86 | StringBuilder sb = new StringBuilder(); | ||
| 87 | String k = null; | ||
| 88 | while (idx < v.length()) { | ||
| 89 | int nextEq = v.indexOf('=', idx); | ||
| 90 | int nextEscape = v.indexOf('\\', idx); | ||
| 91 | int next = optMin(nextEq, nextEscape); | ||
| 92 | if (next == -1) { | ||
| 93 | break; | ||
| 94 | } else if (next == nextEq) { | ||
| 95 | sb.append(v, idx, nextEq); | ||
| 96 | k = sb.toString(); | ||
| 97 | sb.delete(0, sb.length()); | ||
| 98 | idx = nextEq + 1; | ||
| 99 | break; | ||
| 100 | } else if (next == nextEscape) { | ||
| 101 | sb.append(v, idx, nextEscape); | ||
| 102 | idx = parseEscape(v, nextEscape, sb); | ||
| 103 | } | ||
| 104 | } | ||
| 105 | while (idx < v.length()) { | ||
| 106 | int nextEscape = v.indexOf('\\', idx); | ||
| 107 | if (nextEscape != -1) { | ||
| 108 | sb.append(v, idx, nextEscape); | ||
| 109 | idx = parseEscape(v, nextEscape, sb); | ||
| 110 | } else { | ||
| 111 | break; | ||
| 112 | } | ||
| 113 | } | ||
| 114 | sb.append(v, idx, v.length()); | ||
| 115 | if (k == null) return NO_MATCH; | ||
| 116 | visitor.visitKeyValue(k, sb.toString()); | ||
| 117 | return idx; | ||
| 118 | } | ||
| 119 | |||
| 120 | private static int parseEscape(String v, int idx, StringBuilder sb) { | ||
| 121 | if (idx + 1 < v.length()) { | ||
| 122 | if (v.charAt(idx + 1) == 'u') { | ||
| 123 | if (idx + 5 < v.length()) { | ||
| 124 | String codePoint = v.substring(idx + 2, idx + 6); | ||
| 125 | try { | ||
| 126 | int c = Integer.parseUnsignedInt(codePoint, 16); | ||
| 127 | sb.append((char) c); | ||
| 128 | } catch (NumberFormatException ignored) { | ||
| 129 | } | ||
| 130 | idx = idx + 6; | ||
| 131 | } | ||
| 132 | } else if (v.charAt(idx + 1) == 'n') { | ||
| 133 | sb.append('\n'); | ||
| 134 | idx = idx + 2; | ||
| 135 | } else { | ||
| 136 | sb.append(v.charAt(idx + 1)); | ||
| 137 | idx = idx + 2; | ||
| 138 | } | ||
| 139 | } else { | ||
| 140 | idx = idx + 1; | ||
| 141 | } | ||
| 142 | return idx; | ||
| 143 | } | ||
| 144 | |||
| 145 | public static String structureToString(ConfigSection section) { | ||
| 146 | StringBuilder sb = new StringBuilder(); | ||
| 147 | structureToString(section, sb, new ArrayList<>()); | ||
| 148 | return sb.toString(); | ||
| 149 | } | ||
| 150 | |||
| 151 | private static void structureToString(ConfigSection section, StringBuilder sb, List<String> pathStack) { | ||
| 152 | if (!section.values().isEmpty()) { | ||
| 153 | if (sb.length() > 0) sb.append('\n'); | ||
| 154 | pathStack.forEach(n -> sb.append('[').append(escapeSection(n)).append(']')); | ||
| 155 | if (!pathStack.isEmpty()) sb.append('\n'); | ||
| 156 | section.values().entrySet().stream() | ||
| 157 | .sorted(Entry.comparingByKey()) | ||
| 158 | .forEach(e -> sb.append(escapeKey(e.getKey())).append('=').append(escapeValue(e.getValue())).append('\n')); | ||
| 159 | } | ||
| 160 | |||
| 161 | section.sections().entrySet().stream().sorted(Entry.comparingByKey()).forEach(e -> { | ||
| 162 | pathStack.add(e.getKey()); | ||
| 163 | structureToString(e.getValue(), sb, pathStack); | ||
| 164 | pathStack.remove(pathStack.size() - 1); | ||
| 165 | }); | ||
| 166 | } | ||
| 167 | |||
| 168 | private static String escapeSection(String s) { | ||
| 169 | return s | ||
| 170 | .replace("\\", "\\\\") | ||
| 171 | .replace("\n", "\\n") | ||
| 172 | .replace("]", "\\]") | ||
| 173 | .chars().mapToObj(c -> c >= 32 && c < 127 ? Character.toString((char) c) : String.format("\\u%04x", c)).collect(Collectors.joining()); | ||
| 174 | } | ||
| 175 | |||
| 176 | private static String escapeKey(String s) { | ||
| 177 | return s | ||
| 178 | .replace("\\", "\\\\") | ||
| 179 | .replace("[", "\\[") | ||
| 180 | .replace("\n", "\\n") | ||
| 181 | .replace("=", "\\=") | ||
| 182 | .chars().mapToObj(c -> c >= 32 && c < 127 ? Character.toString((char) c) : String.format("\\u%04x", c)).collect(Collectors.joining()); | ||
| 183 | } | ||
| 184 | |||
| 185 | private static String escapeValue(String s) { | ||
| 186 | return s | ||
| 187 | .replace("\\", "\\\\") | ||
| 188 | .replace("\n", "\\n") | ||
| 189 | .chars().mapToObj(c -> c >= 32 && c < 127 ? Character.toString((char) c) : String.format("\\u%04x", c)).collect(Collectors.joining()); | ||
| 190 | } | ||
| 191 | |||
| 192 | public static Optional<Boolean> parseBool(String v) { | ||
| 193 | if (v == null) return Optional.empty(); | ||
| 194 | switch (v) { | ||
| 195 | case "true": | ||
| 196 | return Optional.of(true); | ||
| 197 | case "false": | ||
| 198 | return Optional.of(false); | ||
| 199 | default: | ||
| 200 | return Optional.empty(); | ||
| 201 | } | ||
| 202 | } | ||
| 203 | |||
| 204 | public static OptionalInt parseInt(String v) { | ||
| 205 | if (v == null) return OptionalInt.empty(); | ||
| 206 | try { | ||
| 207 | return OptionalInt.of(Integer.parseInt(v)); | ||
| 208 | } catch (NumberFormatException e) { | ||
| 209 | return OptionalInt.empty(); | ||
| 210 | } | ||
| 211 | } | ||
| 212 | |||
| 213 | public static OptionalDouble parseDouble(String v) { | ||
| 214 | if (v == null) return OptionalDouble.empty(); | ||
| 215 | try { | ||
| 216 | return OptionalDouble.of(Double.parseDouble(v)); | ||
| 217 | } catch (NumberFormatException e) { | ||
| 218 | return OptionalDouble.empty(); | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | public static OptionalInt parseRgbColor(String v) { | ||
| 223 | if (v == null) return OptionalInt.empty(); | ||
| 224 | try { | ||
| 225 | if (FULL_RGB_COLOR.matcher(v).matches()) { | ||
| 226 | return OptionalInt.of(Integer.parseUnsignedInt(v.substring(1), 16)); | ||
| 227 | } else if (MIN_RGB_COLOR.matcher(v).matches()) { | ||
| 228 | int result = Integer.parseUnsignedInt(v.substring(1), 16); | ||
| 229 | // change 0xABC to 0xAABBCC | ||
| 230 | result = (result & 0x00F) | (result & 0x0F0) << 4 | (result & 0xF00) << 8; | ||
| 231 | result = result | result << 4; | ||
| 232 | return OptionalInt.of(result); | ||
| 233 | } else { | ||
| 234 | return OptionalInt.empty(); | ||
| 235 | } | ||
| 236 | } catch (NumberFormatException e) { | ||
| 237 | return OptionalInt.empty(); | ||
| 238 | } | ||
| 239 | } | ||
| 240 | |||
| 241 | public static String rgbColorToString(int color) { | ||
| 242 | color = color & 0xFFFFFF; | ||
| 243 | boolean isShort = ((color & 0xF0F0F0) >> 4 ^ color & 0x0F0F0F) == 0; | ||
| 244 | if (isShort) { | ||
| 245 | int packed = color & 0x0F0F0F; | ||
| 246 | packed = packed & 0xF | packed >> 4; | ||
| 247 | packed = packed & 0xFF | (packed & ~0xFF) >> 4; | ||
| 248 | return String.format("#%03x", packed); | ||
| 249 | } else { | ||
| 250 | return String.format("#%06x", color); | ||
| 251 | } | ||
| 252 | } | ||
| 253 | |||
| 254 | public static Optional<String[]> parseArray(String v) { | ||
| 255 | if (v == null) return Optional.empty(); | ||
| 256 | List<String> l = new ArrayList<>(); | ||
| 257 | int idx = 0; | ||
| 258 | StringBuilder cur = new StringBuilder(); | ||
| 259 | while (true) { | ||
| 260 | int nextSep = v.indexOf(',', idx); | ||
| 261 | int nextEsc = v.indexOf('\\', idx); | ||
| 262 | int next = optMin(nextSep, nextEsc); | ||
| 263 | if (next == -1) { | ||
| 264 | cur.append(v, idx, v.length()); | ||
| 265 | l.add(cur.toString()); | ||
| 266 | return Optional.of(l.toArray(new String[0])); | ||
| 267 | } else if (next == nextSep) { | ||
| 268 | cur.append(v, idx, nextSep); | ||
| 269 | l.add(cur.toString()); | ||
| 270 | cur.delete(0, cur.length()); | ||
| 271 | idx = nextSep + 1; | ||
| 272 | } else if (next == nextEsc) { | ||
| 273 | cur.append(v, idx, nextEsc); | ||
| 274 | if (nextEsc + 1 < v.length()) { | ||
| 275 | cur.append(v.charAt(nextEsc + 1)); | ||
| 276 | } | ||
| 277 | idx = nextEsc + 2; | ||
| 278 | } | ||
| 279 | } | ||
| 280 | } | ||
| 281 | |||
| 282 | public static String arrayToString(String[] values) { | ||
| 283 | return Arrays.stream(values) | ||
| 284 | .map(s -> s.replace("\\", "\\\\").replace(",", "\\,")) | ||
| 285 | .collect(Collectors.joining(",")); | ||
| 286 | } | ||
| 287 | |||
| 288 | public static <T extends Enum<T>> Optional<T> parseEnum(Function<String, T> byName, String v) { | ||
| 289 | if (v == null) return Optional.empty(); | ||
| 290 | try { | ||
| 291 | return Optional.of(byName.apply(v)); | ||
| 292 | } catch (IllegalArgumentException e) { | ||
| 293 | return Optional.empty(); | ||
| 294 | } | ||
| 295 | } | ||
| 296 | |||
| 297 | private static int optMin(int v1, int v2) { | ||
| 298 | if (v1 == -1) return v2; | ||
| 299 | if (v2 == -1) return v1; | ||
| 300 | return Math.min(v1, v2); | ||
| 301 | } | ||
| 302 | |||
| 303 | } | ||
diff --git a/enigma/src/main/java/cuchaz/enigma/config/ConfigStructureVisitor.java b/enigma/src/main/java/cuchaz/enigma/config/ConfigStructureVisitor.java new file mode 100644 index 0000000..12d7ec4 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/config/ConfigStructureVisitor.java | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | package cuchaz.enigma.config; | ||
| 2 | |||
| 3 | public interface ConfigStructureVisitor { | ||
| 4 | |||
| 5 | void visitKeyValue(String key, String value); | ||
| 6 | |||
| 7 | void visitSection(String section); | ||
| 8 | |||
| 9 | void jumpToRootSection(); | ||
| 10 | |||
| 11 | } | ||
diff --git a/enigma/src/main/java/cuchaz/enigma/utils/Os.java b/enigma/src/main/java/cuchaz/enigma/utils/Os.java new file mode 100644 index 0000000..b493c04 --- /dev/null +++ b/enigma/src/main/java/cuchaz/enigma/utils/Os.java | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | package cuchaz.enigma.utils; | ||
| 2 | |||
| 3 | import java.util.Locale; | ||
| 4 | |||
| 5 | public enum Os { | ||
| 6 | LINUX, | ||
| 7 | MAC, | ||
| 8 | SOLARIS, | ||
| 9 | WINDOWS, | ||
| 10 | OTHER; | ||
| 11 | |||
| 12 | private static Os os = null; | ||
| 13 | |||
| 14 | public static Os getOs() { | ||
| 15 | if (os == null) { | ||
| 16 | String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT); | ||
| 17 | if (osName.contains("mac") || osName.contains("darwin")) { | ||
| 18 | os = MAC; | ||
| 19 | } else if (osName.contains("win")) { | ||
| 20 | os = WINDOWS; | ||
| 21 | } else if (osName.contains("nix") || osName.contains("nux") | ||
| 22 | || osName.contains("aix")) { | ||
| 23 | os = LINUX; | ||
| 24 | } else if (osName.contains("sunos")) { | ||
| 25 | os = SOLARIS; | ||
| 26 | } else { | ||
| 27 | os = OTHER; | ||
| 28 | } | ||
| 29 | } | ||
| 30 | return os; | ||
| 31 | } | ||
| 32 | |||
| 33 | } \ No newline at end of file | ||