From 0823b5dbc3f78ef5b7cae7aad672ac4b44872020 Mon Sep 17 00:00:00 2001 From: Tommimon <37435103+Tommimon@users.noreply.github.com> Date: Wed, 24 Jun 2020 23:30:55 +0200 Subject: [PATCH] Included same feature as master e729d4ee458cc412856a44e95130af22e531ea1c but for shell version from 2.8 to 3.5 --- README.md | 31 ++++---- asyncExec.js | 38 ---------- checkPermissions.js | 52 ------------- extension.js | 42 ++--------- myLog.js | 3 - permissionsHandler.js | 84 --------------------- shortcutMaker.js | 165 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 189 insertions(+), 226 deletions(-) delete mode 100644 asyncExec.js delete mode 100644 checkPermissions.js delete mode 100644 myLog.js delete mode 100644 permissionsHandler.js create mode 100644 shortcutMaker.js diff --git a/README.md b/README.md index 659e559..73c64b9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Add to Desktop -An easy way to create desktop app shortcut in gnome +An easy way to create desktop app shortcut in GNOME ## Overview -This simple extension tries to make easier the gnome process to create a desktop +This simple extension tries to make easier the GNOME process to create a desktop shortcut for apps. -The idea is simple: instead of searching for the .desktop files throw multiple -folders let's use the gnome application launcher that already groups all our apps. +The idea is simple: instead of searching for the `.desktop` files through multiple +folders let's use the application launcher that already groups all our apps. This extension adds a new line to the app context menu in the application launcher, the new entry ('Add to Desktop') if clicked automatically creates a desktop shortcut @@ -16,16 +16,19 @@ When you have your shortcut on the desktop you still have to enable it with right-click and 'Allow Launching' as if you create the shortcut manually. ## Dependencies -This extension needs the [desktop-icons](https://extensions.gnome.org/extension/1465/desktop-icons/) +This extension needs the [Desktop Icons](https://extensions.gnome.org/extension/1465/desktop-icons/) extension installed and enabled to work properly. Otherwise you will not be able to see the shortcuts you are creating. +Currently, I'm working to get this extension merged in Desktop Icons project, if you would like +having this feature by default in Desktop Icons extension let them now. + ## Installation -- Download zip from the releases section or gnome-shell extensions site -- Extract in a folder called `add-to-desktop@tommimon.github.com` -- Add the `add-to-desktop@tommimon.github.com` folder to gnome extensions folder* -- Restart the Gnome-Shell** -- Open (or restart) the Extensions app +- Download zip from the releases section +- Extract in a folder named `add-to-desktop@tommimon.github.com` +- Add the `add-to-desktop@tommimon.github.com` folder to GNOME extensions folder* +- Restart the GNOME shell** +- Open (or restart) the Extensions app (or Tweaks app) - Enable 'Add to Desktop' - Enjoy @@ -43,12 +46,12 @@ can get to the windows concept of shortcut. Many people copy the .desktop file to the desktop instead of creating a soft link. I encourage using links because this fixes many issues with application updates: if your application gets updated the behaviour of the `.desktop` file may change, I -experienced app icons changing path upgrading, this may make your shortcut no more +experienced app icons changing path updating, this may make your shortcut no more working properly, and you have to create a new one from zero. With soft links this shouldn't -happen as long as the `.desktop` file used by the application launcher stay in the +happen as long as the `.desktop` file used by the application launcher remains in the same place which usually is the case. -This approach has a dow side: permission issues but now this extension takes care automatically +This approach has a downside: permission issues but now this extension takes care automatically of those problems and changes the launcher file permissions adding the executable -permission if and only when needed. This operation may require root authentication +permission if and only if is needed. This operation may require root authentication if the file is owned by another user. diff --git a/asyncExec.js b/asyncExec.js deleted file mode 100644 index 80f8432..0000000 --- a/asyncExec.js +++ /dev/null @@ -1,38 +0,0 @@ -const Gio = imports.gi.Gio; - -// executes command as root asking for authentication and pass the result to the function onFinish -var PrivilegedExec = function privilegedExec(args, onFinish) { - return NormalExec(['pkexec'].concat(args), onFinish); -} - -// executes command and pass the result to the function onFinish -var NormalExec = function normalExec(args, onFinish) { - try { - let proc = Gio.Subprocess.new( - args, - Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE - ); - - proc.communicate_utf8_async(null, null, (proc, res) => { - let out, err; - - try { - let [, stdout, stderr] = proc.communicate_utf8_finish(res); - - // Failure - if (!proc.get_successful()) - throw new Error(stderr); - - // Success - out = stdout; - } catch (e) { - err = e; - } - - onFinish(out, err); // the async errors and outputs are passed to another function - }); - } catch (e) { - return e; // the immediate errors are returned to caller - } - return null; -} diff --git a/checkPermissions.js b/checkPermissions.js deleted file mode 100644 index 6202d69..0000000 --- a/checkPermissions.js +++ /dev/null @@ -1,52 +0,0 @@ -const ExtensionUtils = imports.misc.extensionUtils; -const Me = ExtensionUtils.getCurrentExtension(); -const MyLog = Me.imports.myLog.MyLog; -const AsyncExec = Me.imports.asyncExec; - -var CheckPermissions = class CheckPermissions { - constructor(appPath, onFinishInit) { - this.appPath = appPath; - this.onFinishInit = onFinishInit; // function to execute when all the permissions are found - this.info = null; - this.current = null; - this.owner = null; - this.ownerExec = false; // execute permission for file owner - this.otherExec = false; - } - - // saves the result of ls -l - initInfo() { - var self = this; - // is inside MyLog to print errors - let error = AsyncExec.NormalExec(["ls", "-l", this.appPath], (out, err) => { - self.info = out; - self.findOwner(); - self.findPermissions(); - self.getCurrent(); - }); - if(error != null) { - MyLog(error); - } - } - - getCurrent() { - var self = this; - let error = AsyncExec.NormalExec(["whoami"], (out, err) => { - self.current = out.replace("\n", ""); - self.onFinishInit(); - }); - if(error != null) { - MyLog(error); - } - } - - findOwner() { - this.owner = this.info.split(" ")[2]; - } - - findPermissions() { - let permissions = this.info.split(" ")[0]; - this.ownerExec = permissions.charAt(3) === "x"; - this.otherExec = permissions.charAt(9) === "x"; - } -} \ No newline at end of file diff --git a/extension.js b/extension.js index 2043f20..372e1b8 100755 --- a/extension.js +++ b/extension.js @@ -1,50 +1,22 @@ const AppDisplay = imports.ui.appDisplay; -const GLib = imports.gi.GLib; const ExtensionUtils = imports.misc.extensionUtils; const Me = ExtensionUtils.getCurrentExtension(); -const PermissionsHandler = Me.imports.permissionsHandler; -const MyLog = Me.imports.myLog.MyLog; +const ShortcutMaker = Me.imports.shortcutMaker; -// var because accessed elsewhere -var DESKTOP_DIRECTORY = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP); - -// Save the standard Menu globally to be able to reset it -var ParentMenu; +// Saves the standard Menu globally to be able to reset it on disable +var parentMenu = null; function init () { - ParentMenu = AppDisplay.AppIconMenu; - MyLog("init completed"); -} -function insertCustomEntry(menu) { - // Add the "Add to Desktop" entry to the menu - menu._appendSeparator(); - let item = menu._appendMenuItem("Add to Desktop"); - item.connect('activate', () => { - let appPath = menu._source.app.get_app_info().get_filename(); // get the .desktop file complete path - MyLog(appPath); - let handler = new PermissionsHandler.PermissionsHandler(appPath); - handler.start(); - }); } function enable () { - // AppIconMenu is the var which contains the class used to instantiate the context menu - // Assigning my own custom menu that extends the standard menu so the custom will be used for instantiate - AppDisplay.AppIconMenu = class customMenu extends ParentMenu { - _redisplay() { - super._redisplay(); - insertCustomEntry(this); - } - } - - MyLog("DESKTOP_DIRECTORY = " + DESKTOP_DIRECTORY); - MyLog("enable completed"); + parentMenu = AppDisplay.AppIconMenu; + ShortcutMaker.addShortcutButton(parentMenu); } function disable () { - // Reset the menu to the standard one (not customized) - AppDisplay.AppIconMenu = ParentMenu; - MyLog("disable completed"); + // Reset the menu to the standard one (without new item) + AppDisplay.AppIconMenu = parentMenu; } diff --git a/myLog.js b/myLog.js deleted file mode 100644 index 4a9e4bf..0000000 --- a/myLog.js +++ /dev/null @@ -1,3 +0,0 @@ -var MyLog = function myLog(text) { - log("add-to-desktop: " + text); -} diff --git a/permissionsHandler.js b/permissionsHandler.js deleted file mode 100644 index 6244167..0000000 --- a/permissionsHandler.js +++ /dev/null @@ -1,84 +0,0 @@ -const GLib = imports.gi.GLib; - -const ExtensionUtils = imports.misc.extensionUtils; -const Me = ExtensionUtils.getCurrentExtension(); -const MyLog = Me.imports.myLog.MyLog; -const AsyncExec = Me.imports.asyncExec; -const Extension = Me.imports.extension; -const CheckPermissions = Me.imports.checkPermissions; - -var PermissionsHandler = class PermissionsHandler { - constructor(appPath) { - this.appPath = appPath; - this.permissions = new CheckPermissions.CheckPermissions(this.appPath, () => { - self.handlePermissions(); // I'm setting the action to do after initInfo() - }); - var self = this; - } - - // starts the whole process - start() { - this.permissions.initInfo(); // get permission info and than start working on them - } - - // requires authentication (if needed) and creates link - handlePermissions() { - MyLog("current = " + this.permissions.current); - MyLog("owner = " + this.permissions.owner); - MyLog("owner_can = " + this.permissions.ownerExec); - MyLog("other_can = " + this.permissions.otherExec); - - if(this.permissions.otherExec) { - MyLog("No extra permissions needed"); - this.createLink(); - } - else { - if(this.permissions.current === this.permissions.owner) { - if(this.permissions.ownerExec) { - MyLog("No extra permissions needed"); - this.createLink(); - } - else { - MyLog("Adding execute permission for this user"); - this.fixPermissions(); - } - } - else { - MyLog("Protected file, adding execute permission for every user"); - this.sudoFixPermissions(); - } - } - } - - fixPermissions() { - // adding to all instead to other is preferred because it makes more sense - let args = ["chmod", "u+x", this.appPath]; - AsyncExec.NormalExec(args, (out, err) => { - this.createLink(); - }); - } - - sudoFixPermissions() { - let args = ["chmod", "a+x", this.appPath]; - AsyncExec.PrivilegedExec(args, (out, err) => { - this.chmodCompleted(out, err); - }); - } - - chmodCompleted(out, err) { - // if we have successfully changed the permissions we create the link - if(err === undefined) { - MyLog("Authenticated"); - this.createLink(this.appPath); - } - else { - MyLog("Authentication failed"); - } - } - - createLink() { - // create a soft symbolic link on the desktop - GLib.spawn_command_line_async("ln -s " + this.appPath + " " + Extension.DESKTOP_DIRECTORY); - } - -} diff --git a/shortcutMaker.js b/shortcutMaker.js new file mode 100644 index 0000000..fd26cc1 --- /dev/null +++ b/shortcutMaker.js @@ -0,0 +1,165 @@ +const Gio = imports.gi.Gio; +const GLib = imports.gi.GLib; +const AppDisplay = imports.ui.appDisplay; + +var S_IWOTH = 0o00002; + +// executes command and pass the result to the callback function +function execAsync(args, callback) { + let proc = Gio.Subprocess.new( + args, + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE + ); + + proc.communicate_utf8_async(null, null, (proc, res) => { + let out, err; + try { + let [, stdout, stderr] = proc.communicate_utf8_finish(res); + // Failure + if (!proc.get_successful()) + err = stderr; + // Success + out = stdout; + } catch (e) { + err = e; + } + callback(out, err); // the async errors and outputs are passed to another function + }); +} + +// override the context menu rebuild method to add the new item +function addShortcutButton(parentMenu) { + AppDisplay.AppIconMenu = class CustomMenu extends parentMenu { + // use _rebuildMenu() instead for shell version 3.36 or later + _redisplay() { + super._redisplay(); + this._appendSeparator(); + // Add the "Add to Desktop" item to the menu + let item = this._appendMenuItem('Add to Desktop'); + item.connect('activate', () => { + let appPath = this._source.app.get_app_info().get_filename(); // get the .desktop file complete path + let maker = new ShortcutMaker(appPath); + maker.start(); + }); + } + } +} + +// decides which permission are required, adds them, creates the shortcut and allows launching +class ShortcutMaker { + constructor(appPath) { + this._appPath = appPath; + this._desktop = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DESKTOP); + this._info = new LauncherInfo(appPath); + this._current = GLib.get_user_name(); + } + + start() { + this._info.initInfo(() => {this._decideAction()}); + } + + + _decideAction() { + log('Current user = ' + this._current); + log('Owner = ' + this._info.owner); + log('Can execute = ' + this._info.canExecute); + log('Writable by others = ' + this._info.writableByOthers); + + if(this._current === this._info.owner) { + if(this._info.writableByOthers) { + log('Removing write bit for others'); + this._fixPermissions(); + } + else { + log('No extra permissions needed'); + this._createLink(); + } + } + else { + let fixWrite = this._info.writableByOthers; + let fixExec = !this._info.canExecute; + if(fixWrite || fixExec) { + log('Requiring authentication to fix permissions'); + this._sudoFixPermissions(fixWrite, fixExec); + } + else { + log('No extra permissions needed'); + this._createLink(); + } + } + } + + _fixPermissions() { + // the only action needed is remove writable by others + let args = ['chmod', 'o-w', this._appPath]; + execAsync(args, (out, err) => { + if(err === undefined) { + this._createLink(); + } + else { + log(err); // unexpected error + } + }); + } + + calcMode(fixWrite, fixExec) { + let mode = ''; + if(fixWrite) { + mode = 'o-w'; + if(fixExec) { + mode = 'o-w,a+x'; + } + } + else { + if(fixExec) { + mode = 'a+x'; + } + } + return mode; + } + + // require permission before executing command as root, using Gio subprocess to use pkexec + _sudoFixPermissions(fixWrite, fixExec) { + let mode = this.calcMode(fixWrite, fixExec); + let args = ['pkexec', 'chmod', mode, this._appPath]; + execAsync(args, (out, err) => { + // if we have successfully changed the permissions we create the link + if(err === undefined) { + log('Authenticated successfully'); + this._createLink(this._appPath); + } + else { + log(err); // may be request dismissed + } + }); + } + + _createLink() { + // create a soft symbolic link on the desktop + execAsync(['ln', '-s', this._appPath, this._desktop], () => {}); + } +} + +// contains the info about the .desktop file +class LauncherInfo { + constructor(appPath) { + this.file = Gio.File.new_for_path(appPath); + } + + initInfo(callback) { + this.file.query_info_async( + 'access::can-execute,owner::user,unix::mode', + Gio.FileQueryInfoFlags.NONE, + GLib.PRIORITY_DEFAULT, + null, + (source, result) => { + let fileInfo = source.query_info_finish(result); + this.owner = fileInfo.get_attribute_as_string('owner::user'); + this.canExecute = fileInfo.get_attribute_boolean('access::can-execute'); + let unixMode = fileInfo.get_attribute_uint32('unix::mode'); + this.writableByOthers = (unixMode & S_IWOTH) != 0; + callback(); + } + ) + } +}