kgx-window.c 27.5 KB
Newer Older
Zander Brown's avatar
Zander Brown committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/* kgx-window.c
 *
 * Copyright 2019 Zander Brown
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

Zander Brown's avatar
Zander Brown committed
19 20 21 22
/**
 * SECTION:kgx-window
 * @title: KgxWindow
 * @short_description: Window
Zander Brown's avatar
Zander Brown committed
23 24
 * 
 * The main #GtkApplicationWindow that acts as the terminal
25 26
 * 
 * Since: 0.1.0
Zander Brown's avatar
Zander Brown committed
27 28
 */

Zander Brown's avatar
Zander Brown committed
29 30
#define G_LOG_DOMAIN "Kgx"

Zander Brown's avatar
Zander Brown committed
31
#include <glib/gi18n.h>
Zander Brown's avatar
Zander Brown committed
32
#include <vte/vte.h>
Zander Brown's avatar
Zander Brown committed
33 34
#define PCRE2_CODE_UNIT_WIDTH 0
#include <pcre2.h>
Zander Brown's avatar
Zander Brown committed
35
#include <math.h>
Zander Brown's avatar
Zander Brown committed
36

Zander Brown's avatar
Zander Brown committed
37
#include "rgba.h"
38
#include "fp-vte-util.h"
Zander Brown's avatar
Zander Brown committed
39

Zander Brown's avatar
Zander Brown committed
40 41
#include "kgx-config.h"
#include "kgx-window.h"
Zander Brown's avatar
Zander Brown committed
42
#include "kgx-application.h"
43
#include "kgx-search-box.h"
Zander Brown's avatar
Zander Brown committed
44
#include "kgx-process.h"
45
#include "kgx-close-dialog.h"
Zander Brown's avatar
Zander Brown committed
46

47 48 49
#define KGX_WINDOW_STYLE_ROOT "root"
#define KGX_WINDOW_STYLE_REMOTE "remote"

Zander Brown's avatar
Zander Brown committed
50 51
G_DEFINE_TYPE (KgxWindow, kgx_window, GTK_TYPE_APPLICATION_WINDOW)

Zander Brown's avatar
Zander Brown committed
52
enum {
53 54 55
  PROP_0,
  PROP_THEME,
  PROP_INITIAL_WORK_DIR,
Zander Brown's avatar
Zander Brown committed
56
  PROP_COMMAND,
57
  PROP_CLOSE_ON_ZERO,
58
  LAST_PROP
Zander Brown's avatar
Zander Brown committed
59 60 61 62
};

static GParamSpec *pspecs[LAST_PROP] = { NULL, };

Zander Brown's avatar
Zander Brown committed
63

Zander Brown's avatar
Zander Brown committed
64
static void
Zander Brown's avatar
Zander Brown committed
65 66 67 68
kgx_window_set_theme (KgxWindow *self,
                      KgxTheme   theme)
{
  GdkRGBA fg;
69
  GdkRGBA bg = (GdkRGBA) { 0.1, 0.1, 0.1, 0.96};
Zander Brown's avatar
Zander Brown committed
70

Zander Brown's avatar
Zander Brown committed
71 72 73 74 75 76
  // Workings of GDK_RGBA prevent this being static
  GdkRGBA palette[16] = {
    GDK_RGBA ("241f31"), // Black
    GDK_RGBA ("c01c28"), // Red
    GDK_RGBA ("2ec27e"), // Green
    GDK_RGBA ("f5c211"), // Yellow
Zander Brown's avatar
Zander Brown committed
77 78
    GDK_RGBA ("1e78e4"), // Blue
    GDK_RGBA ("9841bb"), // Magenta
Zander Brown's avatar
Zander Brown committed
79 80 81 82 83 84
    GDK_RGBA ("0ab9dc"), // Cyan
    GDK_RGBA ("c0bfbc"), // White
    GDK_RGBA ("5e5c64"), // Bright Black
    GDK_RGBA ("ed333b"), // Bright Red
    GDK_RGBA ("57e389"), // Bright Green
    GDK_RGBA ("f8e45c"), // Bright Yellow
Zander Brown's avatar
Zander Brown committed
85
    GDK_RGBA ("51a1ff"), // Bright Blue
Zander Brown's avatar
Zander Brown committed
86 87 88 89 90
    GDK_RGBA ("c061cb"), // Bright Magenta
    GDK_RGBA ("4fd2fd"), // Bright Cyan
    GDK_RGBA ("f6f5f4"), // Bright White
  };

Zander Brown's avatar
Zander Brown committed
91 92 93 94 95 96
  self->theme = theme;

  switch (theme) {
    case KGX_THEME_HACKER:
      fg = (GdkRGBA) { 0.1, 1.0, 0.1, 1.0};
      break;
97
    case KGX_THEME_NIGHT:
Zander Brown's avatar
Zander Brown committed
98
    default:
99
      fg = (GdkRGBA) { 1.0, 1.0, 1.0, 1.0};
Zander Brown's avatar
Zander Brown committed
100 101 102
      break;
  }

103 104 105 106 107
  vte_terminal_set_colors (VTE_TERMINAL (self->terminal),
                           &fg,
                           &bg,
                           palette,
                           16);
Zander Brown's avatar
Zander Brown committed
108 109 110 111

  g_object_notify_by_pspec (G_OBJECT (self), pspecs[PROP_THEME]);
}

Zander Brown's avatar
Zander Brown committed
112
static void
Zander Brown's avatar
Zander Brown committed
113 114 115
wait_cb (GPid     pid,
         gint     status,
         gpointer user_data)
Zander Brown's avatar
Zander Brown committed
116 117 118

{
  KgxWindow *self = KGX_WINDOW (user_data);
Zander Brown's avatar
Zander Brown committed
119
  #if HAS_GTOP
Zander Brown's avatar
Zander Brown committed
120
  GtkApplication *app = NULL;
Zander Brown's avatar
Zander Brown committed
121
  #endif
Zander Brown's avatar
Zander Brown committed
122 123 124
  g_autoptr (GError) error = NULL;
  GtkStyleContext *context = NULL;

Zander Brown's avatar
Zander Brown committed
125 126
  g_return_if_fail (KGX_IS_WINDOW (self));

Zander Brown's avatar
Zander Brown committed
127
  #if HAS_GTOP
Zander Brown's avatar
Zander Brown committed
128 129
  app = gtk_window_get_application (GTK_WINDOW (self));

Zander Brown's avatar
Zander Brown committed
130 131 132 133
  // If the application is in closing the app may already be null
  if (app) {
    kgx_application_remove_watch (KGX_APPLICATION (app), pid);
  }
Zander Brown's avatar
Zander Brown committed
134
  #endif
Zander Brown's avatar
Zander Brown committed
135

Zander Brown's avatar
Zander Brown committed
136 137 138 139 140
  // If the window has already closed we can't do much
  if (self->exit_info == NULL) {
    return;
  }

Zander Brown's avatar
Zander Brown committed
141 142 143 144
  /* wait_check will set @error if it got a signal/non-zero exit */
  if (!g_spawn_check_exit_status (status, &error)) {
    g_autofree char *message = NULL;

Zander Brown's avatar
Zander Brown committed
145 146
    context = gtk_widget_get_style_context (GTK_WIDGET (self->exit_info));

147 148 149 150
    // translators: <b> </b> marks the text as bold, ensure they are
    // matched please!
    message = g_strdup_printf (_("<b>Read Only</b> — Command exited with code %i"),
                               status);
Zander Brown's avatar
Zander Brown committed
151 152 153 154 155 156 157 158

    gtk_label_set_markup (GTK_LABEL (self->exit_message), message);
    gtk_style_context_add_class (context, "error");
  } else if (self->close_on_zero) {
    gtk_widget_destroy (GTK_WIDGET (self));

    return;
  } else {
Zander Brown's avatar
Zander Brown committed
159 160
    context = gtk_widget_get_style_context (GTK_WIDGET (self->exit_info));

Zander Brown's avatar
Zander Brown committed
161
    gtk_label_set_markup (GTK_LABEL (self->exit_message),
162 163
    // translators: <b> </b> marks the text as bold, ensure they are
    // matched please!
Zander Brown's avatar
Zander Brown committed
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
                         _("<b>Read Only</b> — Command exited"));
    gtk_style_context_remove_class (context, "error");
  }

  gtk_revealer_set_reveal_child (GTK_REVEALER (self->exit_info), TRUE);
}

static void
spawned (VtePty       *pty,
         GAsyncResult *res,
         gpointer      data)

{
  g_autoptr(GError) error = NULL;
  KgxWindow *self = KGX_WINDOW (data);
Zander Brown's avatar
Zander Brown committed
179
  #if HAS_GTOP
Zander Brown's avatar
Zander Brown committed
180
  GtkApplication *app = NULL;
Zander Brown's avatar
Zander Brown committed
181
  #endif
Zander Brown's avatar
Zander Brown committed
182 183 184 185 186 187 188 189
  GPid pid;

  g_return_if_fail (VTE_IS_PTY (pty));
  g_return_if_fail (G_IS_ASYNC_RESULT (res));

  fp_vte_pty_spawn_finish (pty, res, &pid, &error);

  if (error) {
190
    GtkStyleContext *context;
Zander Brown's avatar
Zander Brown committed
191 192
    g_autofree char *message = NULL;

Piotr Drąg's avatar
Piotr Drąg committed
193
    g_critical ("Failed to spawn: %s", error->message);
Zander Brown's avatar
Zander Brown committed
194

195 196 197
    context = gtk_widget_get_style_context (GTK_WIDGET (self->exit_info));

    gtk_style_context_add_class (context, "error");
Zander Brown's avatar
Zander Brown committed
198

199 200 201 202
    // translators: <b> </b> marks the text as bold, ensure they are
    // matched please!
    message = g_strdup_printf (_("<b>Failed to start</b> — %s"),
                               error->message);
Zander Brown's avatar
Zander Brown committed
203 204 205 206 207 208 209 210 211
   
    gtk_label_set_markup (GTK_LABEL (self->exit_message), message);

    gtk_revealer_set_reveal_child (GTK_REVEALER (self->exit_info), TRUE);

    return;
  }

  #if HAS_GTOP
Zander Brown's avatar
Zander Brown committed
212 213 214 215
  app = gtk_window_get_application (GTK_WINDOW (self));

  kgx_application_add_watch (KGX_APPLICATION (app),
                             pid,
Zander Brown's avatar
Zander Brown committed
216 217 218 219 220 221 222 223 224
                             KGX_WINDOW (self));
  #endif

  g_child_watch_add (pid, wait_cb, self);
}

static void
kgx_window_constructed (GObject *object)
{
Zander Brown's avatar
Zander Brown committed
225 226 227 228
  KgxWindow          *self = KGX_WINDOW (object);
  const char         *initial = NULL;
  g_autoptr (VtePty)  pty = NULL;
  g_autoptr (GError)  error = NULL;
Zander Brown's avatar
Zander Brown committed
229
  g_auto (GStrv)      shell = NULL;
Zander Brown's avatar
Zander Brown committed
230
  g_auto (GStrv)      env = NULL;
Zander Brown's avatar
Zander Brown committed
231
  g_autofree char    *command = NULL;
Zander Brown's avatar
Zander Brown committed
232 233 234

  pty = vte_pty_new_sync (fp_vte_pty_default_flags (), NULL, &error);

Zander Brown's avatar
Zander Brown committed
235
  if (G_UNLIKELY (self->command != NULL)) {
Zander Brown's avatar
Zander Brown committed
236
    // dup the string so we can free command later to handle the
Piotr Drąg's avatar
Piotr Drąg committed
237
    // (more likely) fp_vte_guess_shell case
Zander Brown's avatar
Zander Brown committed
238
    command = g_strdup (self->command);
Zander Brown's avatar
Zander Brown committed
239
  } else {
Zander Brown's avatar
Zander Brown committed
240
    command = fp_vte_guess_shell (NULL, &error);
Zander Brown's avatar
Zander Brown committed
241 242 243 244 245
    if (error) {
      g_warning ("flatterm: %s", error->message);
    }
  }

Zander Brown's avatar
Zander Brown committed
246 247
  if (command == NULL) {
    command = g_strdup ("/bin/sh");
Zander Brown's avatar
Zander Brown committed
248
    g_warning ("Defaulting to %s", shell[0]);
Zander Brown's avatar
Zander Brown committed
249 250
  }

Zander Brown's avatar
Zander Brown committed
251 252 253 254 255
  g_shell_parse_argv (command, NULL, &shell, &error);
  if (error) {
    g_warning ("Can't handle %s: %s", command, error->message);
  }

Zander Brown's avatar
Zander Brown committed
256 257 258 259 260 261
  if (self->working_dir) {
    initial = self->working_dir;
  } else {
    initial = g_get_home_dir ();
  }

Zander Brown's avatar
Zander Brown committed
262
  g_debug ("Working in %s", initial);
Zander Brown's avatar
Zander Brown committed
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278

  env = g_environ_setenv (env, "TERM", "xterm-256color", TRUE);

  vte_terminal_set_pty (VTE_TERMINAL (self->terminal), pty);
  fp_vte_pty_spawn_async (pty,
                          initial,
                          (const gchar * const *) shell,
                          (const gchar * const *) env,
                          -1,
                          NULL,
                          (GAsyncReadyCallback) spawned,
                          self);

  G_OBJECT_CLASS (kgx_window_parent_class)->constructed (object);
}

Zander Brown's avatar
Zander Brown committed
279 280 281 282 283 284 285 286 287 288 289 290
static void
kgx_window_set_property (GObject      *object,
                         guint         property_id,
                         const GValue *value,
                         GParamSpec   *pspec)
{
  KgxWindow *self = KGX_WINDOW (object);

  switch (property_id) {
    case PROP_THEME:
      kgx_window_set_theme (self, g_value_get_enum (value));
      break;
291
    case PROP_INITIAL_WORK_DIR:
Zander Brown's avatar
Zander Brown committed
292 293 294 295
      self->working_dir = g_value_dup_string (value);
      break;
    case PROP_COMMAND:
      self->command = g_value_dup_string (value);
296
      break;
297 298 299
    case PROP_CLOSE_ON_ZERO:
      self->close_on_zero = g_value_get_boolean (value);
      break;
Zander Brown's avatar
Zander Brown committed
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

static void
kgx_window_get_property (GObject    *object,
                         guint       property_id,
                         GValue     *value,
                         GParamSpec *pspec)
{
  KgxWindow *self = KGX_WINDOW (object);

  switch (property_id) {
    case PROP_THEME:
      g_value_set_enum (value, self->theme);
      break;
Zander Brown's avatar
Zander Brown committed
318 319 320 321 322 323
    case PROP_INITIAL_WORK_DIR:
      g_value_set_string (value, self->working_dir);
      break;
    case PROP_COMMAND:
      g_value_set_string (value, self->command);
      break;
324 325 326
    case PROP_CLOSE_ON_ZERO:
      g_value_set_boolean (value, self->close_on_zero);
      break;
Zander Brown's avatar
Zander Brown committed
327 328 329 330 331 332
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
  }
}

Zander Brown's avatar
Zander Brown committed
333 334 335 336 337 338 339 340
static void
kgx_window_finalize (GObject *object)
{
  KgxWindow *self = KGX_WINDOW (object);

  g_clear_pointer (&self->working_dir, g_free);
  g_clear_pointer (&self->command, g_free);

341 342 343
  g_clear_pointer (&self->root, g_hash_table_unref);
  g_clear_pointer (&self->remote, g_hash_table_unref);

Zander Brown's avatar
Zander Brown committed
344 345 346
  G_OBJECT_CLASS (kgx_window_parent_class)->finalize (object);
}

Zander Brown's avatar
Zander Brown committed
347
#if HAS_GTOP
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
static void
delete_response (GtkWidget *dlg,
                 int        response,
                 KgxWindow *self)
{
  gtk_widget_destroy (dlg);

  if (response == GTK_RESPONSE_OK) {
    self->close_anyway = TRUE;

    gtk_widget_destroy (GTK_WIDGET (self));
  }
}

static gboolean
kgx_window_delete_event (GtkWidget   *widget,
                         GdkEventAny *event)
{
  KgxWindow *self = KGX_WINDOW (widget);
  GHashTableIter iter;
  GtkWidget *dlg;
  gpointer pid, process;

  if (g_hash_table_size (self->children) < 1 || self->close_anyway) {
    return FALSE; // Aka no, I don't want to block closing
  }

  dlg = g_object_new (KGX_TYPE_CLOSE_DIALOG,
                      "transient-for", self,
                      "use-header-bar", TRUE,
                      NULL);

  g_signal_connect (dlg, "response", G_CALLBACK (delete_response), self);

  g_hash_table_iter_init (&iter, self->children);
  while (g_hash_table_iter_next (&iter, &pid, &process)) {
    kgx_close_dialog_add_command (KGX_CLOSE_DIALOG (dlg),
                                  kgx_process_get_exec (process));
  }

  gtk_widget_show (dlg);
  
  return TRUE; // Block the close
}
Zander Brown's avatar
Zander Brown committed
392 393 394 395 396 397 398 399
#else
static gboolean
kgx_window_delete_event (GtkWidget   *widget,
                         GdkEventAny *event)
{
  return FALSE; // Aka no, I don't want to block closing
}
#endif
400

Zander Brown's avatar
Zander Brown committed
401 402 403
static void
application_set (GObject *object, GParamSpec *pspec, gpointer data)
{
404 405 406 407 408
  GtkApplication *app;

  app = gtk_window_get_application (GTK_WINDOW (object));

  g_object_bind_property (app,
Zander Brown's avatar
Zander Brown committed
409 410 411 412
                          "theme",
                          object,
                          "theme",
                          G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
413 414 415 416 417 418

  g_object_bind_property (app,
                          "font",
                          KGX_WINDOW (object)->terminal,
                          "font-desc",
                          G_BINDING_SYNC_CREATE);
Zander Brown's avatar
Zander Brown committed
419 420 421 422 423 424

  g_object_bind_property (app,
                          "font-scale",
                          KGX_WINDOW (object)->terminal,
                          "font-scale",
                          G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
Zander Brown's avatar
Zander Brown committed
425 426
}

Zander Brown's avatar
Zander Brown committed
427 428 429 430 431 432 433 434 435 436 437 438 439 440
static void
active_changed (GObject *object, GParamSpec *pspec, gpointer data)
{
  GtkApplication *app;

  app = gtk_window_get_application (GTK_WINDOW (object));

  if (gtk_window_is_active (GTK_WINDOW (object))) {
    kgx_application_push_active (KGX_APPLICATION (app));
  } else {
    kgx_application_pop_active (KGX_APPLICATION (app));
  }
}

441 442 443 444 445 446 447 448 449 450 451 452
static void
search_enabled (GObject    *object,
                GParamSpec *pspec,
                KgxWindow  *self)
{
  if (gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (self->search_bar))) {
    kgx_search_box_focus (KGX_SEARCH_BOX (self->search_wrap));
  } else {
    gtk_widget_grab_focus (self->terminal);
  }
}

Zander Brown's avatar
Zander Brown committed
453
static void
454 455 456
search_changed (KgxSearchBox *box,
                const gchar  *search,
                KgxWindow    *self)
Zander Brown's avatar
Zander Brown committed
457 458 459 460
{
  VteRegex *regex;
  GError *error = NULL;

461
  regex = vte_regex_new_for_search (g_regex_escape_string (search, -1),
Zander Brown's avatar
Zander Brown committed
462
                                    -1, PCRE2_MULTILINE, &error);
Zander Brown's avatar
Zander Brown committed
463 464

  if (error) {
Piotr Drąg's avatar
Piotr Drąg committed
465
    g_warning ("Search error: %s", error->message);
Zander Brown's avatar
Zander Brown committed
466 467 468 469 470 471 472 473
    return;
  }

  vte_terminal_search_set_regex (VTE_TERMINAL (self->terminal),
                                 regex, 0);
}

static void
474 475
search_next (KgxSearchBox *box,
             KgxWindow    *self)
Zander Brown's avatar
Zander Brown committed
476 477 478 479 480
{
  vte_terminal_search_find_next (VTE_TERMINAL (self->terminal));
}

static void
481 482
search_prev (KgxSearchBox *box,
             KgxWindow    *self)
Zander Brown's avatar
Zander Brown committed
483 484 485
{
  vte_terminal_search_find_previous (VTE_TERMINAL (self->terminal));
}
Zander Brown's avatar
Zander Brown committed
486

487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
static void
font_increase (VteTerminal *term,
               KgxWindow   *self)
{
  GAction *action = NULL;

  action = g_action_map_lookup_action (G_ACTION_MAP (self), "zoom-in");
  g_action_activate (action, NULL);
}

static void
font_decrease (VteTerminal *term,
               KgxWindow   *self)
{
  GAction *action = NULL;

  action = g_action_map_lookup_action (G_ACTION_MAP (self), "zoom-out");
  g_action_activate (action, NULL);
}

Zander Brown's avatar
Zander Brown committed
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535
static void
update_actions (KgxWindow *self)
{
  VteTerminal *term = VTE_TERMINAL (self->terminal);
  gdouble current = vte_terminal_get_font_scale (term);
  GAction *action;

  action = g_action_map_lookup_action (G_ACTION_MAP (self), "zoom-out");
  g_simple_action_set_enabled (G_SIMPLE_ACTION (action), current > 0.5);
  action = g_action_map_lookup_action (G_ACTION_MAP (self), "zoom-normal");
  g_simple_action_set_enabled (G_SIMPLE_ACTION (action), current != 1.0);
  action = g_action_map_lookup_action (G_ACTION_MAP (self), "zoom-in");
  g_simple_action_set_enabled (G_SIMPLE_ACTION (action), current < 2.0);
}

static void
font_scaled (VteTerminal *term,
             GParamSpec  *param,
             KgxWindow   *self)
{
  g_autofree char *label = NULL;

  label = g_strdup_printf ("%i%%",
                           (int) round (vte_terminal_get_font_scale (term) * 100));
  gtk_label_set_label (GTK_LABEL (self->zoom_level), label);

  update_actions (self);
}

Zander Brown's avatar
Zander Brown committed
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
static gboolean
size_timeout (KgxWindow *self)
{
  self->timeout = 0;

  gtk_widget_hide (self->dims);

  return G_SOURCE_REMOVE;
}

static void
size_changed (GtkWidget    *widget,
              GdkRectangle *allocation,
              KgxWindow    *self)
{
  char        *label;
  int          cols;
  int          rows;
  VteTerminal *term = VTE_TERMINAL (widget);

556 557 558 559 560
  if (gtk_window_is_maximized (GTK_WINDOW (self))) {
    // Don't show when maximised as it isn't very interesting
    return;
  }

Zander Brown's avatar
Zander Brown committed
561 562 563 564 565 566 567 568 569 570 571 572 573 574
  cols = vte_terminal_get_column_count (term);
  rows = vte_terminal_get_row_count (term);

  if (self->timeout != 0) {
    g_source_remove (self->timeout);
  }
  self->timeout = g_timeout_add (800, G_SOURCE_FUNC (size_timeout), self);

  if (cols == self->last_cols && rows == self->last_rows)
    return;

  self->last_cols = cols;
  self->last_rows = rows;

Zander Brown's avatar
Fix #4  
Zander Brown committed
575
  label = g_strdup_printf ("%i × %i", cols, rows);
Zander Brown's avatar
Zander Brown committed
576 577 578 579 580 581 582

  gtk_label_set_label (GTK_LABEL (self->dims), label);
  gtk_widget_show (self->dims);

  g_free (label);
}

Zander Brown's avatar
Zander Brown committed
583 584 585
static void
kgx_window_class_init (KgxWindowClass *klass)
{
Zander Brown's avatar
Zander Brown committed
586
  GObjectClass   *object_class = G_OBJECT_CLASS (klass);
Zander Brown's avatar
Zander Brown committed
587 588
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

Zander Brown's avatar
Zander Brown committed
589
  object_class->constructed = kgx_window_constructed;
Zander Brown's avatar
Zander Brown committed
590 591
  object_class->set_property = kgx_window_set_property;
  object_class->get_property = kgx_window_get_property;
Zander Brown's avatar
Zander Brown committed
592
  object_class->finalize = kgx_window_finalize;
Zander Brown's avatar
Zander Brown committed
593

594 595 596 597 598 599 600 601 602
  widget_class->delete_event = kgx_window_delete_event;

  /**
   * KgxWindow:theme:
   * 
   * The #KgxTheme used in the window, is bound to #KgxApplication:theme
   * 
   * Since: 0.1.0
   */
Zander Brown's avatar
Zander Brown committed
603 604 605 606 607
  pspecs[PROP_THEME] =
    g_param_spec_enum ("theme", "Theme", "Terminal theme",
                       KGX_TYPE_THEME, KGX_THEME_HACKER,
                       G_PARAM_READWRITE);

608 609 610 611 612 613 614 615

  /**
   * KgxWindow:initial-work-dir:
   * 
   * Used to handle --working-dir
   * 
   * Since: 0.1.0
   */
616 617 618 619
  pspecs[PROP_INITIAL_WORK_DIR] =
    g_param_spec_string ("initial-work-dir", "Initial directory",
                         "Initial working directory",
                         NULL,
Zander Brown's avatar
Zander Brown committed
620 621
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);

622 623 624 625 626 627 628
  /**
   * KgxWindow:command:
   * 
   * Used to handle -e
   * 
   * Since: 0.1.0
   */
Zander Brown's avatar
Zander Brown committed
629 630 631 632 633
  pspecs[PROP_COMMAND] =
    g_param_spec_string ("command", "Command",
                         "Command to run",
                         NULL,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
634

635 636 637 638 639 640 641
  /**
   * KgxWindow:close-on-zero:
   * 
   * Should the window autoclose when the terminal complete
   * 
   * Since: 0.1.0
   */
642 643 644 645 646 647
  pspecs[PROP_CLOSE_ON_ZERO] =
    g_param_spec_boolean ("close-on-zero", "Close on zero",
                          "Should close when child exits with 0",
                          TRUE,
                          G_PARAM_READWRITE);

648
  g_object_class_install_properties (object_class, LAST_PROP, pspecs);
Zander Brown's avatar
Zander Brown committed
649

650 651
  gtk_widget_class_set_template_from_resource (widget_class,
                                               RES_PATH "kgx-window.ui");
Zander Brown's avatar
Zander Brown committed
652

Zander Brown's avatar
Zander Brown committed
653 654
  gtk_widget_class_bind_template_child (widget_class, KgxWindow, header_bar);
  gtk_widget_class_bind_template_child (widget_class, KgxWindow, terminal);
655
  gtk_widget_class_bind_template_child (widget_class, KgxWindow, dims);
Zander Brown's avatar
Zander Brown committed
656
  gtk_widget_class_bind_template_child (widget_class, KgxWindow, search_bar);
657
  gtk_widget_class_bind_template_child (widget_class, KgxWindow, search_wrap);
Zander Brown's avatar
Zander Brown committed
658 659
  gtk_widget_class_bind_template_child (widget_class, KgxWindow, exit_info);
  gtk_widget_class_bind_template_child (widget_class, KgxWindow, exit_message);
Zander Brown's avatar
Zander Brown committed
660
  gtk_widget_class_bind_template_child (widget_class, KgxWindow, zoom_level);
Zander Brown's avatar
Zander Brown committed
661

Zander Brown's avatar
Zander Brown committed
662
  gtk_widget_class_bind_template_callback (widget_class, application_set);
Zander Brown's avatar
Zander Brown committed
663
  gtk_widget_class_bind_template_callback (widget_class, active_changed);
Zander Brown's avatar
Zander Brown committed
664

665
  gtk_widget_class_bind_template_callback (widget_class, search_enabled);
Zander Brown's avatar
Zander Brown committed
666 667 668
  gtk_widget_class_bind_template_callback (widget_class, search_changed);
  gtk_widget_class_bind_template_callback (widget_class, search_next);
  gtk_widget_class_bind_template_callback (widget_class, search_prev);
Zander Brown's avatar
Zander Brown committed
669

670 671
  gtk_widget_class_bind_template_callback (widget_class, font_increase);
  gtk_widget_class_bind_template_callback (widget_class, font_decrease);
Zander Brown's avatar
Zander Brown committed
672
  gtk_widget_class_bind_template_callback (widget_class, font_scaled);
673

Zander Brown's avatar
Zander Brown committed
674
  gtk_widget_class_bind_template_callback (widget_class, size_changed);
Zander Brown's avatar
Zander Brown committed
675 676
}

677 678 679 680 681
static void
new_activated (GSimpleAction *action,
               GVariant      *parameter,
               gpointer       data)
{
Zander Brown's avatar
Zander Brown committed
682 683
  GtkWindow       *window = NULL;
  GtkApplication  *app = NULL;
684
  guint32          timestamp;
Zander Brown's avatar
Zander Brown committed
685
  g_autofree char *dir = NULL;
686 687 688 689 690 691 692 693 694

  /* Slightly "wrong" but hopefully by taking the time before
   * we spend non-zero time initing the window it's far enough in the
   * past for shell to do-the-right-thing
   */
  timestamp = GDK_CURRENT_TIME;

  app = gtk_window_get_application (GTK_WINDOW (data));

695 696
  dir = kgx_window_get_working_dir (KGX_WINDOW (data));

697 698
  window = g_object_new (KGX_TYPE_WINDOW,
                        "application", app,
699
                        "initial-work-dir", dir,
700
                        "close-on-zero", TRUE,
701 702 703 704 705
                        NULL);

  gtk_window_present_with_time (window, timestamp);
}

Zander Brown's avatar
Zander Brown committed
706 707 708 709 710
static void
about_activated (GSimpleAction *action,
                 GVariant      *parameter,
                 gpointer       data)
{
Zander Brown's avatar
Zander Brown committed
711 712
  const char *authors[] = { "Zander Brown<zbrown@gnome.org>", NULL };
  const char *artists[] = { "Tobias Bernard", NULL };
713 714 715 716
  g_autofree char *copyright = NULL;
  
  copyright = g_strdup_printf (_("Copyright © %s Zander Brown"),
                               "2019");
Zander Brown's avatar
Zander Brown committed
717 718 719

  gtk_show_about_dialog (GTK_WINDOW (data),
                         "authors", authors,
720
                         "artists", artists,
Zander Brown's avatar
Zander Brown committed
721
                         // Translators: Credit yourself here
Zander Brown's avatar
Zander Brown committed
722
                         "translator-credits", _("translator-credits"),
Zander Brown's avatar
Zander Brown committed
723
                         "comments", _("Terminal Emulator"),
Zander Brown's avatar
Zander Brown committed
724
                         "copyright", copyright,
Zander Brown's avatar
Zander Brown committed
725
                         "license-type", GTK_LICENSE_GPL_3_0,
Zander Brown's avatar
Zander Brown committed
726 727
                         "logo-icon-name", "kgx-original",
                         #if IS_GENERIC
728 729
                         // Translators: "by King’s Cross" here is meaning
                         // author or creator of 'Terminal'
Piotr Drąg's avatar
Piotr Drąg committed
730
                         "program-name", _("Terminal\nby King’s Cross"),
Zander Brown's avatar
Zander Brown committed
731
                         #else
Piotr Drąg's avatar
Piotr Drąg committed
732
                         "program-name", _("King’s Cross"),
Zander Brown's avatar
Zander Brown committed
733
                         #endif
Zander Brown's avatar
Zander Brown committed
734 735
                         "version", PACKAGE_VERSION,
                         NULL);
Zander Brown's avatar
Zander Brown committed
736 737
}

738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772
static void
zoom_out_activated (GSimpleAction *action,
                    GVariant      *parameter,
                    gpointer       data)
{
  KgxWindow *self = KGX_WINDOW (data);
  VteTerminal *term = VTE_TERMINAL (self->terminal);
  gdouble current = vte_terminal_get_font_scale (term);

  vte_terminal_set_font_scale (term, current - 0.1);
}

static void
zoom_normal_activated (GSimpleAction *action,
                       GVariant      *parameter,
                       gpointer       data)
{
  KgxWindow *self = KGX_WINDOW (data);
  VteTerminal *term = VTE_TERMINAL (self->terminal);

  vte_terminal_set_font_scale (term, 1.0);
}

static void
zoom_in_activated (GSimpleAction *action,
                   GVariant      *parameter,
                   gpointer       data)
{
  KgxWindow *self = KGX_WINDOW (data);
  VteTerminal *term = VTE_TERMINAL (self->terminal);
  gdouble current = vte_terminal_get_font_scale (term);

  vte_terminal_set_font_scale (term, current + 0.1);
}

Zander Brown's avatar
Zander Brown committed
773 774
static GActionEntry win_entries[] =
{
775
  { "new-window", new_activated, NULL, NULL, NULL },
Zander Brown's avatar
Zander Brown committed
776
  { "about", about_activated, NULL, NULL, NULL },
777 778 779
  { "zoom-out", zoom_out_activated, NULL, NULL, NULL },
  { "zoom-normal", zoom_normal_activated, NULL, NULL, NULL },
  { "zoom-in", zoom_in_activated, NULL, NULL, NULL },
Zander Brown's avatar
Zander Brown committed
780 781
};

782 783 784 785 786 787 788 789 790
static gboolean
update_subtitle (GBinding     *binding,
                 const GValue *from_value,
                 GValue       *to_value,
                 gpointer      data)
{
  g_autoptr (GFile) file = NULL;
  g_autofree char *path = NULL;
  const char *uri;
Zander Brown's avatar
Zander Brown committed
791
  const char *home = NULL;
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809

  uri = g_value_get_string (from_value);
  if (uri == NULL) {
    g_value_set_string (to_value, NULL);
    return TRUE;
  }

  file = g_file_new_for_uri (uri);

  path = g_file_get_path (file);
  if (path == NULL) {
    g_value_set_string (to_value, NULL);

    return TRUE;
  }

  home = g_get_home_dir ();
  if (g_str_has_prefix (path, home)) {
810 811
    g_autofree char *short_home = g_strdup_printf ("~%s",
                                                   path + strlen (home));
812

Zander Brown's avatar
Zander Brown committed
813
    g_value_set_string (to_value, short_home);
814 815 816 817 818 819 820 821 822

    return TRUE;
  }

  g_value_set_string (to_value, path);

  return TRUE;
}

Zander Brown's avatar
Zander Brown committed
823 824 825
static void
kgx_window_init (KgxWindow *self)
{
Zander Brown's avatar
Zander Brown committed
826
  GPropertyAction *pact;
Zander Brown's avatar
Zander Brown committed
827

Zander Brown's avatar
Zander Brown committed
828 829
  gtk_widget_init_template (GTK_WIDGET (self));

Zander Brown's avatar
Zander Brown committed
830 831 832 833 834
  g_action_map_add_action_entries (G_ACTION_MAP (self),
                                   win_entries,
                                   G_N_ELEMENTS (win_entries),
                                   self);

835 836
  update_actions (self);

837
  self->theme = KGX_THEME_NIGHT;
838
  self->close_on_zero = TRUE;
Zander Brown's avatar
Zander Brown committed
839

840 841
  self->root = g_hash_table_new (g_direct_hash, g_direct_equal);
  self->remote = g_hash_table_new (g_direct_hash, g_direct_equal);
842 843 844 845
  self->children = g_hash_table_new_full (g_direct_hash,
                                          g_direct_equal,
                                          NULL,
                                          (GDestroyNotify) kgx_process_unref);
846

Zander Brown's avatar
Zander Brown committed
847 848
  pact = g_property_action_new ("theme", G_OBJECT (self), "theme");
  g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (pact));
Zander Brown's avatar
Zander Brown committed
849

850 851 852
  pact = g_property_action_new ("find",
                                G_OBJECT (self->search_bar),
                                "search-mode-enabled");
Zander Brown's avatar
Zander Brown committed
853 854
  g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (pact));

855 856 857 858 859 860
  g_object_bind_property_full (self->terminal, "current-directory-uri",
                               self->header_bar, "subtitle",
                               G_BINDING_SYNC_CREATE,
                               update_subtitle,
                               NULL, NULL, NULL);

861 862
  g_object_bind_property (self, "theme",
                          self->terminal, "theme",
Zander Brown's avatar
Zander Brown committed
863 864 865 866
                          G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);

  g_object_bind_property (self, "is-maximized",
                          self->terminal, "opaque",
867
                          G_BINDING_SYNC_CREATE);
Zander Brown's avatar
Zander Brown committed
868
}
869

Zander Brown's avatar
Zander Brown committed
870 871 872 873 874 875
/**
 * kgx_window_get_working_dir:
 * @self: the #KgxWindow
 * 
 * Get the working directory path of this window, used to open new windows
 * in the same directory
876 877
 * 
 * Since: 0.1.0
Zander Brown's avatar
Zander Brown committed
878
 */
Zander Brown's avatar
Zander Brown committed
879
char *
880 881 882
kgx_window_get_working_dir (KgxWindow *self)
{
  const char *uri;
883
  g_autoptr (GFile) file = NULL;
884 885 886 887 888 889 890 891

  g_return_val_if_fail (KGX_IS_WINDOW (self), NULL);

  uri = vte_terminal_get_current_directory_uri (VTE_TERMINAL (self->terminal));
  file = g_file_new_for_uri (uri);

  return g_file_get_path (file);
}
892

Zander Brown's avatar
Zander Brown committed
893
#if HAS_GTOP
894 895 896 897 898 899
static inline void
push_type (GHashTable      *table,
           GPid             pid,
           KgxProcess      *process,
           GtkStyleContext *context,
           const char      *class_name)
900
{
901 902
  g_hash_table_insert (table,
                       GINT_TO_POINTER (pid),
903
                       process != NULL ? g_rc_box_acquire (process) : NULL);
904

905
  g_debug ("Now %i %s", g_hash_table_size (table), class_name);
906

907 908 909
  if (G_LIKELY (class_name != NULL)) {
    gtk_style_context_add_class (context, class_name);
  }
910
}
Zander Brown's avatar
Zander Brown committed
911
#endif
912

Zander Brown's avatar
Zander Brown committed
913
/**
914
 * kgx_window_push_child:
Zander Brown's avatar
Zander Brown committed
915
 * @self: the #KgxWindow
916 917 918
 * @process: the #KgxProcess of the remote process
 * 
 * Registers @pid as a child of @self
Zander Brown's avatar
Zander Brown committed
919
 * 
920
 * Since: 0.2.0
Zander Brown's avatar
Zander Brown committed
921
 */
922
void
923 924
kgx_window_push_child (KgxWindow    *self,
                       KgxProcess   *process)
925
{
Zander Brown's avatar
Zander Brown committed
926
  #if HAS_GTOP
927
  GtkStyleContext *context;
Zander Brown's avatar
Zander Brown committed
928 929
  GPid pid = 0;
  const char *exec = NULL;
Zander Brown's avatar
Zander Brown committed
930

931 932
  g_return_if_fail (KGX_IS_WINDOW (self));

933 934 935
  context = gtk_widget_get_style_context (GTK_WIDGET (self));
  pid = kgx_process_get_pid (process);
  exec = kgx_process_get_exec (process);
936

937
  if (G_UNLIKELY (g_str_has_prefix (exec, "ssh "))) {
938 939
    push_type (self->remote, pid, NULL, context, KGX_WINDOW_STYLE_REMOTE);
  }
940

941 942
  if (G_UNLIKELY (kgx_process_get_is_root (process))) {
    push_type (self->root, pid, NULL, context, KGX_WINDOW_STYLE_ROOT);
943
  }
944 945

  push_type (self->children, pid, process, context, NULL);
Zander Brown's avatar
Zander Brown committed
946
  #endif
947 948
}

949 950 951 952 953
inline static void
pop_type (GHashTable      *table,
          GPid             pid,
          GtkStyleContext *context,
          const char      *class_name)
954
{
955
  guint size = 0;
956

957
  g_hash_table_remove (table, GINT_TO_POINTER (pid));
958

959
  size = g_hash_table_size (table);
960

961 962 963 964 965 966 967 968
  if (G_LIKELY (size <= 0)) {
    if (G_LIKELY (class_name)) {
      gtk_style_context_remove_class (context, class_name);
    }
    g_debug ("No longer %s", class_name);
  } else {
    g_debug ("%i %s remaining", size, class_name);
  }
969 970
}

Zander Brown's avatar
Zander Brown committed
971
/**
972
 * kgx_window_pop_child:
Zander Brown's avatar
Zander Brown committed
973
 * @self: the #KgxWindow
974
 * @process: the #KgxProcess of the child process
Zander Brown's avatar
Zander Brown committed
975
 * 
976 977 978
 * Remove a child added with kgx_window_push_child()
 * 
 * Since: 0.2.0
Zander Brown's avatar
Zander Brown committed
979
 */
980
void
981 982
kgx_window_pop_child (KgxWindow    *self,
                      KgxProcess   *process)
983
{
984
  GtkStyleContext *context;
Zander Brown's avatar
Zander Brown committed
985
  GPid pid = 0;
Zander Brown's avatar
Zander Brown committed
986

987 988
  g_return_if_fail (KGX_IS_WINDOW (self));

989
  context = gtk_widget_get_style_context (GTK_WIDGET (self));
Zander Brown's avatar
Zander Brown committed
990
  #if HAS_GTOP
991
  pid = kgx_process_get_pid (process);
Zander Brown's avatar
Zander Brown committed
992
  #endif
993 994 995 996
  
  pop_type (self->remote, pid, context, KGX_WINDOW_STYLE_REMOTE);
  pop_type (self->root, pid, context, KGX_WINDOW_STYLE_ROOT);
  pop_type (self->children, pid, context, NULL);
997
}