├── .eslintrc ├── README.md └── grrr.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | 6 | "ecmaFeatures": { 7 | "arrowFunctions": true, 8 | "binaryLiterals": false, 9 | "blockBindings": true, 10 | "classes": false, 11 | "defaultParams": true, 12 | "destructuring": true, 13 | "forOf": true, 14 | "generators": false, 15 | "modules": false, 16 | "objectLiteralComputedProperties": false, 17 | "objectLiteralDuplicateProperties": false, 18 | "objectLiteralShorthandMethods": false, 19 | "objectLiteralShorthandProperties": false, 20 | "octalLiterals": false, 21 | "regexUFlag": false, 22 | "regexYFlag": true, 23 | "spread": false, 24 | "superInFunctions": false, 25 | "templateStrings": false 26 | }, 27 | 28 | "globals": { 29 | "log": true, 30 | "logError": true, 31 | "print": true, 32 | "printerr": true, 33 | "imports": true, 34 | "ARGV": true 35 | }, 36 | 37 | "rules": { 38 | "comma-dangle": 2, 39 | "no-cond-assign": 2, 40 | "no-console": 1, 41 | "no-constant-condition": 2, 42 | "no-control-regex": 2, 43 | "no-debugger": 2, 44 | "no-dupe-args": 2, 45 | "no-dupe-keys": 2, 46 | "no-duplicate-case": 2, 47 | "no-empty-character-class": 2, 48 | "no-empty": 2, 49 | "no-ex-assign": 2, 50 | "no-extra-boolean-cast": 2, 51 | "no-extra-parens": 0, 52 | "no-extra-semi": 2, 53 | "no-func-assign": 2, 54 | "no-inner-declarations": 2, 55 | "no-invalid-regexp": 2, 56 | "no-irregular-whitespace": 2, 57 | "no-negated-in-lhs": 2, 58 | "no-obj-calls": 2, 59 | "no-regex-spaces": 2, 60 | "no-reserved-keys": 0, 61 | "no-sparse-arrays": 2, 62 | "no-unreachable": 2, 63 | "use-isnan": 2, 64 | "valid-jsdoc": 1, 65 | "valid-typeof": 2, 66 | "no-unexpected-multiline": 2, 67 | 68 | "accessor-pairs": 0, 69 | "block-scoped-var": 2, 70 | "complexity": [1, 11], 71 | "consistent-return": 2, 72 | "curly": [2, "multi-line"], 73 | "default-case": 0, 74 | "dot-notation": 2, 75 | "dot-location": [1, "property"], 76 | "eqeqeq": 2, 77 | "guard-for-in": 0, 78 | "no-alert": 2, 79 | "no-caller": 2, 80 | "no-div-regex": 2, 81 | "no-else-return": 0, 82 | "no-empty-label": 0, 83 | "no-eq-null": 2, 84 | "no-eval": 2, 85 | "no-extend-native": 2, 86 | "no-extra-bind": 2, 87 | "no-fallthrough": 2, 88 | "no-floating-decimal": 2, 89 | "no-implied-eval": 2, 90 | "no-iterator": 2, 91 | "no-labels": 0, 92 | "no-lone-blocks": 2, 93 | "no-loop-func": 2, 94 | "no-multi-spaces": 1, 95 | "no-multi-str": 2, 96 | "no-native-reassign": 2, 97 | "no-new-func": 2, 98 | "no-new-wrappers": 2, 99 | "no-new": 2, 100 | "no-octal-escape": 2, 101 | "no-octal": 2, 102 | "no-octal-escape": 2, 103 | "no-param-reassign": 1, 104 | "no-process-env": 0, 105 | "no-proto": 2, 106 | "no-redeclare": 2, 107 | "no-return-assign": 2, 108 | "no-script-url": 2, 109 | "no-self-compare": 2, 110 | "no-sequences": 2, 111 | "no-unused-expressions": 2, 112 | "no-void": 2, 113 | "no-warning-comments": 0, 114 | "no-with": 2, 115 | "radix": 1, 116 | "vars-on-top": 0, 117 | "wrap-iife": 1, 118 | "yoda": [1, "never"], 119 | 120 | "strict": 0, 121 | 122 | "no-catch-shadow": 2, 123 | "no-delete-var": 2, 124 | "no-label-var": 2, 125 | "no-shadow": 2, 126 | "no-shadow-restricted-names": 2, 127 | "no-undef": 2, 128 | "no-undef-init": 2, 129 | "no-undefined": 2, 130 | "no-unused-vars": 2, 131 | "no-use-before-define": 2, 132 | 133 | "handle-callback-err": 1, 134 | "no-mixed-requires": 1, 135 | "no-new-require": 0, 136 | "no-path-concat": 0, 137 | "no-process-exit": 0, 138 | "no-restricted-modules": 0, 139 | "no-sync": 0, 140 | 141 | "array-bracket-spacing": [1, "always"], 142 | "brace-style": [1, "1tbs", { "allowSingleLine": true }], 143 | "camelcase": 0, 144 | "comma-spacing": [1, { "before": false, "after": true }], 145 | "comma-style": [1, "last"], 146 | "computed-property-spacing": [1, "never"], 147 | "consistent-this": [1, "self"], 148 | "eol-last": 1, 149 | "func-names": 0, 150 | "func-style": 0, 151 | "indent": [1, 4, { "indentSwitchCase": true }], 152 | "key-spacing": [ 1, { "beforeColon": false, "afterColon": true } ], 153 | "lines-around-comment": 1, 154 | "linebreak-style": 1, 155 | "max-nested-callbacks": [1, 3], 156 | "new-cap": 1, 157 | "new-parens": 2, 158 | "newline-after-var": 0, 159 | "no-array-constructor": 1, 160 | "no-continue": 0, 161 | "no-inline-comments": 0, 162 | "no-lonely-if": 0, 163 | "no-mixed-spaces-and-tabs": [1, "smart-tabs"], 164 | "no-multiple-empty-lines": 1, 165 | "no-nested-ternary": 1, 166 | "no-new-object": 1, 167 | "no-spaced-func": 1, 168 | "no-ternary": 0, 169 | "no-trailing-spaces": 1, 170 | "no-underscore-dangle": 0, 171 | "one-var": 1, 172 | "operator-assignment": 1, 173 | "operator-linebreak": [2, "after"], 174 | "padded-blocks": 0, 175 | "quote-props": [1, "as-needed"], 176 | "quotes": [1, "double", "avoid-escape"], 177 | "semi-spacing": [2, { "before": false, "after": true }], 178 | "semi": 2, 179 | "sort-vars": 0, 180 | "space-after-keywords": 1, 181 | "space-before-blocks": 1, 182 | "space-in-parens": [1, "never"], 183 | "space-infix-ops": [1, { "int32Hint": false} ], 184 | "space-return-throw-case": 1, 185 | "space-unary-ops": [1, { "words": true, "nonwords": false }], 186 | "spaced-comment": [1, "always"], 187 | "wrap-regex": 0, 188 | 189 | "constructor-super": 0, 190 | "generator-star-spacing": 0, 191 | "no-this-before-super": 0, 192 | "no-var": 2, 193 | "object-shorthand": 0, 194 | "prefer-const": 0 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Grrr! 2 | ===== 3 | 4 | Grrr! is a small tool to generate gresource files. 5 | 6 | Why the name? Well, generating gresource files is boring, and it also starts with "gr" :) 7 | 8 | If you don't know what GResource is, check the [official documentation](https://developer.gnome.org/gio/stable/GResource.html). 9 | 10 | ## Dependencies 11 | 12 | * gjs 13 | * glib-compile-schemas 14 | * gdk-pixbuf-pixdata 15 | 16 | ## Usage 17 | 18 | Just drag and drop files and folders onto the "Grrr!" window and it'll generate the gresource file. It's that simple! 19 | 20 | You can set the default file name and prefix path from the menu on the top right. 21 | 22 | Happy Grrr...ing! 23 | -------------------------------------------------------------------------------- /grrr.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/gjs 2 | 3 | const GLib = imports.gi.GLib; 4 | const Gio = imports.gi.Gio; 5 | const Gdk = imports.gi.Gdk; 6 | const Gtk = imports.gi.Gtk; 7 | const Notify = imports.gi.Notify; 8 | const Lang = imports.lang; 9 | 10 | const APP_NAME = "Grrr!"; 11 | 12 | Notify.init(APP_NAME); 13 | 14 | const res_name_default = "custom.gresource"; 15 | const res_prefix_default = "/org/gnome/custom"; 16 | 17 | let res_name = res_name_default; 18 | let res_prefix = res_prefix_default; 19 | 20 | let config = {}; 21 | 22 | let config_file = Gio.File.new_for_path(GLib.get_user_data_dir() + "/grrr/config.json"); 23 | 24 | if (config_file.query_exists(null)) { 25 | let size = config_file.query_info("standard::size", 26 | Gio.FileQueryInfoFlags.NONE, 27 | null).get_size(); 28 | 29 | try { 30 | let data = config_file.read(null).read_bytes(size, null).get_data(); 31 | 32 | config = JSON.parse(data); 33 | 34 | if (config.res_name) { 35 | res_name = config.res_name; 36 | } 37 | 38 | if (config.res_prefix) { 39 | res_prefix = config.res_prefix; 40 | } 41 | } catch (e) { 42 | printerr(e); 43 | } 44 | } 45 | 46 | function GResource() { 47 | this._name = res_name; 48 | this._prefix = res_prefix; 49 | 50 | this._files = []; 51 | } 52 | 53 | GResource.prototype.set_name = function(name) { 54 | this._name = name; 55 | }; 56 | 57 | GResource.prototype.set_prefix = function(prefix) { 58 | this._prefix = prefix; 59 | }; 60 | 61 | GResource.prototype.add = function(dir) { 62 | this._base = this._base || dir.get_parent(); 63 | 64 | if (dir.query_info("standard::*", 65 | Gio.FileQueryInfoFlags.NONE, 66 | null).get_file_type() !== Gio.FileType.DIRECTORY) { 67 | 68 | this._files.push(dir); 69 | 70 | return; 71 | } 72 | 73 | let fileEnum; 74 | 75 | try { 76 | fileEnum = dir.enumerate_children("standard::name,standard::type", 77 | Gio.FileQueryInfoFlags.NONE, null); 78 | } catch (e) { 79 | fileEnum = null; 80 | } 81 | 82 | if (fileEnum !== null) { 83 | let info; 84 | 85 | while ((info = fileEnum.next_file(null)) !== null) { 86 | let file = dir.resolve_relative_path(info.get_name()); 87 | 88 | if (info.get_file_type() === Gio.FileType.DIRECTORY) { 89 | this.add(file); 90 | } else { 91 | this._files.push(file); 92 | } 93 | } 94 | } 95 | }; 96 | 97 | GResource.prototype.build = function() { 98 | let xml = "\n"; 99 | 100 | xml += "\n\t\n"; 101 | 102 | for (let file of this._files) { 103 | let info = file.query_info("standard::*", Gio.FileQueryInfoFlags.NONE, null); 104 | 105 | xml += "\t\t"; 106 | 107 | let path = this._base.get_relative_path(file) 108 | .replace(/&/g, "&") 109 | .replace(//g, ">") 111 | .replace(/"/g, """) 112 | .replace(/'/g, "'"); 113 | 114 | if (/image\//.test(info.get_content_type())) { 115 | xml += "" + path + "\n"; 116 | } else { 117 | xml += "" + path + "\n"; 118 | } 119 | } 120 | 121 | xml += "\t\n\n"; 122 | 123 | let xmlfile = this._base.resolve_relative_path(this._name + ".xml"); 124 | 125 | if (xmlfile.query_exists(null)) { 126 | xmlfile.delete(null); 127 | } 128 | 129 | let outputstream = xmlfile.create(Gio.FileCreateFlags.REPLACE_DESTINATION, null); 130 | 131 | outputstream.write_all(xml, null); 132 | 133 | outputstream.close(null); 134 | }; 135 | 136 | GResource.prototype.compile = function(cb = () => {}) { 137 | let ok, pid; 138 | 139 | try { 140 | [ ok, pid ] = GLib.spawn_async(this._base.get_path(), 141 | [ "glib-compile-resources", this._name + ".xml" ], 142 | GLib.get_environ(), 143 | GLib.SpawnFlags.SEARCH_PATH_FROM_ENVP | GLib.SpawnFlags.DO_NOT_REAP_CHILD, 144 | null); 145 | } catch (e) { 146 | printerr(e); 147 | } 148 | 149 | if (ok === false) { 150 | return; 151 | } 152 | 153 | if (typeof pid === "number") { 154 | GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, () => { 155 | GLib.spawn_close_pid(pid); 156 | 157 | try { 158 | let notification = new Notify.Notification({ 159 | summary: "Gresource file generated!", 160 | body: this._name + " generated at " + this._base.get_path(), 161 | icon_name: "dialog-information" 162 | }); 163 | 164 | notification.set_timeout(1000); 165 | 166 | notification.show(); 167 | } catch (e) { 168 | printerr(e); 169 | } 170 | 171 | cb(); 172 | }); 173 | } 174 | }; 175 | 176 | const Application = new Lang.Class({ 177 | Name: APP_NAME, 178 | 179 | _init: function() { 180 | this.application = new Gtk.Application({ 181 | application_id: "org.ozonos.grrr", 182 | flags: Gio.ApplicationFlags.FLAGS_NONE 183 | }); 184 | 185 | this.application.connect("activate", Lang.bind(this, this._onActivate)); 186 | this.application.connect("startup", Lang.bind(this, this._onStartup)); 187 | }, 188 | 189 | _buildUI: function() { 190 | this._window = new Gtk.ApplicationWindow({ 191 | application: this.application, 192 | window_position: Gtk.WindowPosition.CENTER, 193 | title: APP_NAME 194 | }); 195 | 196 | try { 197 | let icon = Gtk.IconTheme.get_default().load_icon("binary", 48, 0); 198 | 199 | this._window.set_icon(icon); 200 | } catch (e) { 201 | printerr(e); 202 | } 203 | 204 | this._headerbar = new Gtk.HeaderBar({ 205 | title: APP_NAME, 206 | show_close_button: true 207 | }); 208 | 209 | // Add options to set the name and the prefix 210 | let grid = new Gtk.Grid({ 211 | column_spacing: 10, 212 | row_spacing: 10, 213 | margin: 10 214 | }); 215 | 216 | grid.set_column_homogeneous(true); 217 | 218 | let namelabel = new Gtk.Label({ label: "File name:" }); 219 | 220 | namelabel.set_halign(Gtk.Align.END); 221 | 222 | let nameentry = new Gtk.Entry(); 223 | 224 | nameentry.connect("changed", () => res_name = nameentry.get_text()); 225 | 226 | nameentry.set_placeholder_text(res_name_default); 227 | 228 | grid.attach(namelabel, 0, 0, 1, 1); 229 | grid.attach_next_to(nameentry, namelabel, Gtk.PositionType.RIGHT, 2, 1); 230 | 231 | let prefixlabel = new Gtk.Label({ label: "Resource prefix:" }); 232 | 233 | prefixlabel.set_halign(Gtk.Align.END); 234 | 235 | let prefixentry = new Gtk.Entry(); 236 | 237 | prefixentry.set_placeholder_text(res_prefix_default); 238 | 239 | prefixentry.connect("changed", () => res_prefix = prefixentry.get_text()); 240 | 241 | grid.attach(prefixlabel, 0, 1, 1, 1); 242 | grid.attach_next_to(prefixentry, prefixlabel, Gtk.PositionType.RIGHT, 2, 1); 243 | 244 | let menubutton = new Gtk.ToggleButton(); 245 | 246 | menubutton.add(new Gtk.Image({ 247 | icon_name: "open-menu-symbolic", 248 | icon_size: Gtk.IconSize.SMALL_TOOLBAR 249 | })); 250 | 251 | menubutton.connect("clicked", () => { 252 | if (menubutton.get_active()) { 253 | menu.show_all(); 254 | } 255 | }); 256 | 257 | let menu = new Gtk.Popover(); 258 | 259 | menu.set_relative_to(menubutton); 260 | 261 | menu.connect("show", () => { 262 | nameentry.set_text(res_name); 263 | prefixentry.set_text(res_prefix); 264 | }); 265 | 266 | menu.connect("closed", () => { 267 | if (menubutton.get_active()) { 268 | menubutton.set_active(false); 269 | } 270 | 271 | res_name = res_name || res_name_default; 272 | res_prefix = res_prefix || res_prefix_default; 273 | 274 | let write = false; 275 | 276 | if (config.res_name !== res_name) { 277 | config.res_name = res_name; 278 | 279 | write = true; 280 | } 281 | 282 | if (config.res_prefix !== res_prefix) { 283 | config.res_prefix = res_prefix; 284 | 285 | write = true; 286 | } 287 | 288 | if (write) { 289 | let parent = config_file.get_parent(); 290 | 291 | if (parent.query_exists(null)) { 292 | if (config_file.query_exists(null)) { 293 | config_file.delete(null); 294 | } 295 | } else { 296 | parent.make_directory_with_parents(null); 297 | } 298 | 299 | let outputstream = config_file.create(Gio.FileCreateFlags.REPLACE_DESTINATION, null); 300 | 301 | outputstream.write_all(JSON.stringify(config), null); 302 | 303 | outputstream.close(null); 304 | } 305 | }); 306 | 307 | menu.add(grid); 308 | 309 | this._headerbar.pack_end(menubutton); 310 | 311 | let spinner = new Gtk.Spinner({ active: true }); 312 | 313 | spinner.set_size_request(64, 64); 314 | 315 | let label = new Gtk.Label({ label: "Drop files and folders to generate a gresource file!" }); 316 | 317 | // Let's set up our window for drag 'n drop 318 | let dnd = new Gtk.Box(); 319 | 320 | dnd.set_vexpand(true); 321 | dnd.set_hexpand(true); 322 | 323 | dnd.drag_dest_set(Gtk.DestDefaults.ALL, null, Gdk.DragAction.COPY); 324 | 325 | dnd.drag_dest_add_text_targets(); 326 | 327 | let generate = (uris) => { 328 | let gresource = new GResource(); 329 | 330 | for (let uri of uris) { 331 | gresource.add(Gio.File.new_for_uri(uri)); 332 | } 333 | 334 | gresource.build(); 335 | gresource.compile(() => { 336 | let complete = new Gtk.Label({ label: res_name + " generated!" }); 337 | 338 | dnd.set_center_widget(complete); 339 | 340 | dnd.show_all(); 341 | 342 | GLib.timeout_add(GLib.PRIORITY_DEFAULT, 3000, () => { 343 | dnd.set_center_widget(label); 344 | 345 | dnd.show_all(); 346 | 347 | return false; 348 | }, null); 349 | }); 350 | 351 | }; 352 | 353 | dnd.connect("drag_data_received", (s, c, x, y, selection) => { 354 | 355 | dnd.set_center_widget(spinner); 356 | 357 | dnd.show_all(); 358 | 359 | let text = selection.get_text(); 360 | 361 | if (text) { 362 | generate(text.split("\n").map(u => u.trim()).filter(u => !!u)); 363 | } 364 | }); 365 | 366 | dnd.set_center_widget(label); 367 | 368 | let addbutton = new Gtk.Button(); 369 | 370 | addbutton.add(new Gtk.Image({ 371 | icon_name: "list-add-symbolic", 372 | icon_size: Gtk.IconSize.SMALL_TOOLBAR 373 | })); 374 | 375 | addbutton.connect("clicked", () => { 376 | let chooser = new Gtk.FileChooserDialog({ 377 | title: "Select folders", 378 | action: Gtk.FileChooserAction.SELECT_FOLDER, 379 | transient_for: this._window, 380 | modal: true 381 | }); 382 | 383 | chooser.set_select_multiple(true); 384 | 385 | chooser.add_button("Add", Gtk.ResponseType.OK); 386 | chooser.add_button("Cancel", Gtk.ResponseType.CANCEL); 387 | 388 | chooser.set_default_response(Gtk.ResponseType.OK); 389 | 390 | chooser.connect("response", (dialog, response) => { 391 | let uris = dialog.get_uris(); 392 | 393 | dialog.destroy(); 394 | 395 | if (response === Gtk.ResponseType.OK && uris && uris.length) { 396 | generate(uris); 397 | } 398 | }); 399 | 400 | chooser.run(); 401 | }); 402 | 403 | this._headerbar.pack_start(addbutton); 404 | 405 | // Add some styles 406 | let css = new Gtk.CssProvider(); 407 | 408 | css.load_from_data("* { font-size: large; }"); 409 | 410 | dnd.get_style_context().add_provider(css, 0); 411 | 412 | this._window.add(dnd); 413 | 414 | this._window.set_default_size(800, 600); 415 | this._window.set_titlebar(this._headerbar); 416 | this._window.show_all(); 417 | }, 418 | 419 | _onActivate: function() { 420 | this._window.present(); 421 | }, 422 | 423 | _onStartup: function() { 424 | this._buildUI(); 425 | } 426 | }); 427 | 428 | let app = new Application(); 429 | 430 | app.application.run(ARGV); 431 | --------------------------------------------------------------------------------