1// -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*-
2/*
3* Copyright (c) 2011-2017 elementary LLC. (https://elementary.io)
4*
5* This program is free software; you can redistribute it and/or
6* modify it under the terms of the GNU Lesser General Public
7* License version 3, as published by the Free Software Foundation.
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 GNU
12* General Public License for more details.
13*
14* You should have received a copy of the GNU Lesser General Public
15* License along with this program; if not, write to the
16* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17* Boston, MA 02110-1301 USA
18*/
19
20public class Terminal.Application : Gtk.Application {
21    public static GLib.Settings saved_state;
22    public static GLib.Settings settings;
23    public static GLib.Settings settings_sys;
24
25    private GLib.List <MainWindow> windows;
26
27    public static string? working_directory = null;
28    [CCode (array_length = false, array_null_terminated = true)]
29    private static string[]? command_e = null;
30    private static string? command_x = null;
31
32    // option_help will be true if help flag was given.
33    private static bool option_help = false;
34    private static bool option_version = false;
35
36    // option_new_window will be true if the new-window flag was given.
37    private static bool option_new_window = false;
38
39    // option_new_tab will be true if the new-tab flag was given.
40    private static bool option_new_tab = false;
41
42    public int minimum_width;
43    public int minimum_height;
44
45    static construct {
46        saved_state = new GLib.Settings ("io.elementary.terminal.saved-state");
47        settings = new GLib.Settings ("io.elementary.terminal.settings");
48        settings_sys = new GLib.Settings ("org.gnome.desktop.interface");
49    }
50
51    construct {
52        flags |= ApplicationFlags.HANDLES_COMMAND_LINE;
53        application_id = "io.elementary.terminal";  /* Ensures only one instance runs */
54
55        Intl.setlocale (LocaleCategory.ALL, "");
56        Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALEDIR);
57        Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8");
58        Intl.textdomain (Config.GETTEXT_PACKAGE);
59    }
60
61    public Application () {
62        Granite.Services.Logger.initialize ("PantheonTerminal");
63        Granite.Services.Logger.DisplayLevel = Granite.Services.LogLevel.DEBUG;
64
65        windows = new GLib.List <MainWindow> ();
66    }
67
68    public void new_window () {
69        var window = get_last_window ();
70
71        if (window == null) {
72            new MainWindow (this);
73        } else {
74            new MainWindow (this, false);
75        }
76    }
77
78    public override int command_line (ApplicationCommandLine command_line) {
79        // keep the application running until we are done with this commandline
80        hold ();
81        int res = _command_line (command_line);
82        release ();
83        return res;
84    }
85
86    public override void window_added (Gtk.Window window) {
87        windows.append (window as MainWindow);
88        base.window_added (window);
89    }
90
91    public override void window_removed (Gtk.Window window) {
92        windows.remove (window as MainWindow);
93        base.window_removed (window);
94    }
95
96    public override bool dbus_register (DBusConnection connection, string object_path) throws Error {
97        base.dbus_register (connection, object_path);
98
99        var dbus = new DBus ();
100        connection.register_object (object_path, dbus);
101
102        dbus.finished_process.connect ((id, process, exit_status) => {
103            foreach (var window in windows) {
104                foreach (var terminal in window.terminals) {
105                    if (terminal.terminal_id == id) {
106
107                        if (!terminal.is_init_complete ()) {
108                            terminal.set_init_complete ();
109                        } else {
110                            var process_string = _("Process completed");
111                            var process_icon = new ThemedIcon ("process-completed-symbolic");
112                            if (exit_status != 0) {
113                                process_string = _("Process exited with errors");
114                                process_icon = new ThemedIcon ("process-error-symbolic");
115                            }
116
117                            if (terminal != window.current_terminal) {
118                                terminal.tab.icon = process_icon;
119                            }
120
121                            if ((window.get_window ().get_state () & Gdk.WindowState.FOCUSED) == 0) {
122                                var notification = new Notification (process_string);
123                                notification.set_body (process);
124                                notification.set_icon (process_icon);
125                                send_notification (null, notification);
126                            }
127                        }
128
129                    }
130                }
131            }
132        });
133
134        return true;
135    }
136
137    private int _command_line (ApplicationCommandLine command_line) {
138        var context = new OptionContext (null);
139        context.add_main_entries (ENTRIES, "pantheon-terminal");
140        context.add_group (Gtk.get_option_group (true));
141
142        // Disable automatic help to prevent default `exit(0)` behaviour.
143        context.set_help_enabled (false);
144
145        string[] args = command_line.get_arguments ();
146        string commandline = "";
147        string[] arg_opt = {};
148        string[] arg_cmd = {};
149        bool build_cmdline = false;
150
151        /* Everything after "--" or "-x" or "--commandline=" is to be treated as a single command to be executed
152         * (maybe with its own options) so it is not passed to the parser.  It will be passed as is to a new tab/shell.
153         */
154        foreach (unowned string s in args) {
155            if (build_cmdline) {
156                arg_cmd += s;
157            } else {
158                if (s == "--" || s == "-x" || s.has_prefix ("--commandline=")) {
159                    if (s.has_prefix ("--commandline=") && s.length > 14) {
160                        arg_cmd += s.substring (14);
161                    }
162
163                    build_cmdline = true;
164                } else {
165                    arg_opt += s;
166                }
167            }
168        }
169
170        commandline = string.joinv (" ", arg_cmd);
171
172        try {
173            unowned string[] tmp = arg_opt;
174            context.parse (ref tmp);
175        } catch (Error e) {
176            stdout.printf ("pantheon-terminal: ERROR: " + e.message + "\n");
177            return 0;
178        }
179
180        if (option_help) {
181            command_line.print (context.get_help (true, null));
182        } else if (option_version) {
183            command_line.print ("%s %s", Config.PROJECT_NAME, Config.VERSION + "\n\n");
184        } else {
185            if (command_e != null) {
186                run_commands (command_e, working_directory);
187            } else if (commandline.length > 0) {
188                run_command_line (commandline, working_directory);
189            } else if (command_x != null) {
190                const string WARNING = "Usage: --commandline=[COMMANDLINE] without spaces around '='\r\n\r\n";
191                start_terminal_with_working_directory (working_directory);
192                get_last_window ().current_terminal.feed (WARNING.data);
193            } else {
194                start_terminal_with_working_directory (working_directory);
195            }
196        }
197
198        // Do not save the value until the next instance of
199        // Pantheon Terminal is started
200        command_e = null;
201        command_x = null;
202        option_help = false;
203        option_new_window = false;
204        option_new_tab = false;
205        working_directory = null;
206
207        return 0;
208    }
209
210    private void run_commands (string[] commands, string? working_directory = null) {
211        MainWindow? window;
212        window = get_last_window ();
213
214        if (window == null || option_new_window) {
215            window = new MainWindow (this, false);
216        }
217
218        foreach (string command in commands) {
219            window.add_tab_with_command (command, working_directory, option_new_tab);
220        }
221    }
222
223    private void run_command_line (string command_line, string? working_directory = null) {
224        MainWindow? window;
225        window = get_last_window ();
226
227        if (window == null || option_new_window) {
228            window = new MainWindow (this, false);
229        }
230
231        window.add_tab_with_command (command_line, working_directory, option_new_tab);
232    }
233
234    private void start_terminal_with_working_directory (string? working_directory) {
235        MainWindow? window;
236        window = get_last_window ();
237
238        if (window != null && !option_new_window) {
239            window.add_tab_with_working_directory (working_directory, null, option_new_tab);
240            window.present ();
241        } else
242            /* Uncertain whether tabs should be restored when app is launched with working directory from commandline.
243             * Currently they are set to restore (subject to the restore-tabs setting).
244             * If it is desired that tabs should never be restored in these circimstances set 3rd parameter to false
245             * below. */
246            new MainWindow.with_working_directory (this, working_directory, window == null, option_new_tab);
247    }
248
249    private MainWindow? get_last_window () {
250        uint length = windows.length ();
251
252        return length > 0 ? windows.nth_data (length - 1) : null;
253    }
254
255    private const OptionEntry[] ENTRIES = {
256        { "version", 'v', 0, OptionArg.NONE, ref option_version, N_("Show version"), null },
257        /* -e flag is used for running single string commands. May be more than one -e flag in cmdline */
258        { "execute", 'e', 0, OptionArg.STRING_ARRAY, ref command_e, N_("Run a program in terminal"), "COMMAND" },
259
260        /* -x flag is removed before OptionContext parser applied but is included here so that it appears in response
261         *  to  the --help flag */
262        { "commandline", 'x', 0, OptionArg.STRING, ref command_x,
263          N_("Run remainder of line as a command in terminal. Can also use '--' as flag"), "COMMAND_LINE" },
264
265        /* -n flag forces a new window, instead of a new tab */
266        { "new-window", 'n', 0, OptionArg.NONE, ref option_new_window, N_("Open a new terminal window"), null },
267
268        /* -t flag forces a new tab  */
269        { "new-tab", 't', 0, OptionArg.NONE, ref option_new_tab, N_("Open a new terminal tab"), null },
270
271        { "help", 'h', 0, OptionArg.NONE, ref option_help, N_("Show help"), null },
272        { "working-directory", 'w', 0, OptionArg.FILENAME, ref working_directory,
273          N_("Set shell working directory"), "DIR" },
274
275        { null }
276    };
277
278    public static int main (string[] args) {
279        var app = new Terminal.Application ();
280        return app.run (args);
281    }
282}
283