1/* 2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> 3 * 4 * This program is free software; you can redistribute it and/or modify 5 * it under the terms of the GNU General Public License as published by 6 * the Free Software Foundation; either version 2 of the License, or 7 * (at your option) any later version. 8 * 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * 14 * You should have received a copy of the GNU General Public License 15 * along with this program; if not, write to the Free Software 16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 17 * 18 * Authored by Michal Hruby <michal.mhr@gmail.com> 19 * 20 */ 21 22namespace Synapse 23{ 24 public class CommandPlugin : Object, Activatable, ItemProvider 25 { 26 public bool enabled { get; set; default = true; } 27 28 public void activate () 29 { 30 31 } 32 33 public void deactivate () 34 { 35 36 } 37 38 private class CommandObject : ApplicationMatch 39 { 40 public string command { get; construct set; } 41 42 public CommandObject (string cmd) 43 { 44 Object (title: cmd, description: _("Run command"), command: cmd, 45 icon_name: "application-x-executable", 46 needs_terminal: cmd.has_prefix ("sudo ")); 47 48 try 49 { 50 app_info = AppInfo.create_from_commandline (cmd, null, 0); 51 } 52 catch (Error err) 53 { 54 warning ("%s", err.message); 55 } 56 } 57 } 58 59 static void register_plugin () 60 { 61 PluginRegistry.get_default ().register_plugin ( 62 typeof (CommandPlugin), 63 "Command Search", 64 _("Find and execute arbitrary commands."), 65 "system-run", 66 register_plugin 67 ); 68 } 69 70 static construct 71 { 72 register_plugin (); 73 } 74 75 private Gee.Set<string> past_commands; 76 private Regex split_regex; 77 78 construct 79 { 80 // TODO: load from configuration 81 past_commands = new Gee.HashSet<string> (); 82 try 83 { 84 split_regex = new Regex ("\\s+", RegexCompileFlags.OPTIMIZE); 85 } 86 catch (RegexError err) 87 { 88 critical ("%s", err.message); 89 } 90 } 91 92 private CommandObject? create_co (string exec) 93 { 94 // ignore results that will be returned by DesktopFilePlugin 95 // and at the same time look for hidden and no-display desktop files, 96 // so we can display their info (title, comment, icon) 97 var dfs = DesktopFileService.get_default (); 98 var df_list = dfs.get_desktop_files_for_exec (exec); 99 DesktopFileInfo? dfi = null; 100 foreach (var df in df_list) 101 { 102 if (!df.is_hidden) return null; // will be handled by App plugin 103 dfi = df; 104 } 105 106 var co = new CommandObject (exec); 107 if (dfi != null) 108 { 109 co.title = dfi.name; 110 if (dfi.comment != "") co.description = dfi.comment; 111 if (dfi.icon_name != null && dfi.icon_name != "") co.icon_name = dfi.icon_name; 112 } 113 114 return co; 115 } 116 117 private void command_executed (Match match) 118 { 119 unowned CommandObject? co = match as CommandObject; 120 if (co == null) return; 121 122 past_commands.add (co.command); 123 } 124 125 public async ResultSet? search (Query q) throws SearchError 126 { 127 // we only search for applications 128 if (!(QueryFlags.APPLICATIONS in q.query_type)) return null; 129 130 Idle.add (search.callback); 131 yield; 132 133 var result = new ResultSet (); 134 135 string stripped = q.query_string.strip (); 136 if (stripped == "") return null; 137 if (stripped.has_prefix ("~/")) 138 { 139 stripped = stripped.replace ("~", Environment.get_home_dir ()); 140 } 141 142 if (!(stripped in past_commands)) 143 { 144 foreach (var command in past_commands) 145 { 146 if (command.has_prefix (stripped)) 147 { 148 result.add (create_co (command), MatchScore.AVERAGE); 149 } 150 } 151 152 string[] args = split_regex.split (stripped); 153 string? valid_cmd = Environment.find_program_in_path (args[0]); 154 155 if (valid_cmd != null) 156 { 157 // don't allow dangerous commands 158 if (args[0] == "rm") return null; 159 CommandObject? co = create_co (stripped); 160 if (co == null) return null; 161 result.add (co, MatchScore.POOR); 162 co.executed.connect (this.command_executed); 163 } 164 } 165 else 166 { 167 result.add (create_co (stripped), MatchScore.VERY_GOOD); 168 } 169 170 q.check_cancellable (); 171 172 return result; 173 } 174 } 175} 176