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