/* SPDX-License-Identifier: Zlib */

#include "shortcuts.h"

#include <girara-gtk/internal.h>
#include <girara-gtk/session.h>
#include <girara-gtk/settings.h>
#include <girara-gtk/shortcuts.h>
#include <girara/datastructures.h>
#include <girara/log.h>
#include <girara/utils.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <math.h>

#include "adjustment.h"
#include "callbacks.h"
#include "commands.h"
#include "database.h"
#include "dbus-interface.h"
#include "document-widget.h"
#include "document.h"
#include "page-widget.h"
#include "page.h"
#include "print.h"
#include "render.h"
#include "utils.h"
#include "zathura.h"

/* Helper function for highlighting the links */
static bool draw_links(zathura_t* zathura) {
  /* set pages to draw links */
  bool show_links                    = false;
  unsigned int page_offset           = 0;
  zathura_document_t* document       = zathura_get_document(zathura);
  const unsigned int number_of_pages = zathura_document_get_number_of_pages(document);
  for (unsigned int page_id = 0; page_id < number_of_pages; page_id++) {
    zathura_page_t* page = zathura_document_get_page(document, page_id);
    if (page == NULL) {
      continue;
    }

    GtkWidget* page_widget   = zathura_page_get_widget(zathura, page);
    GObject* obj_page_widget = G_OBJECT(page_widget);
    g_object_set(obj_page_widget, "draw-search-results", FALSE, NULL);
    if (zathura_page_get_visibility(page) == true) {
      g_object_set(obj_page_widget, "draw-links", TRUE, NULL);

      int number_of_links = 0;
      g_object_get(obj_page_widget, "number-of-links", &number_of_links, NULL);
      if (number_of_links != 0) {
        show_links = true;
      }
      g_object_set(obj_page_widget, "offset-links", page_offset, NULL);
      page_offset += number_of_links;
    } else {
      g_object_set(obj_page_widget, "draw-links", FALSE, NULL);
    }
  }
  return show_links;
}

/* Common code for sc_follow, sc_display_link and sc_copy_link */
static bool link_shortcuts(zathura_t* zathura, girara_callback_inputbar_activate_t callback, const char* text) {
  zathura_document_t* document = zathura_get_document(zathura);
  if (document == NULL || zathura->ui.session == NULL) {
    return false;
  }

  bool show_links = draw_links(zathura);

  /* ask for input */
  if (show_links == true) {
    GtkWidget* inputbar = zathura->ui.session->gtk.inputbar;
    gulong handler_id   = g_signal_connect(inputbar, "hide", G_CALLBACK(cb_hide_links), zathura);
    g_object_set_data(G_OBJECT(inputbar), "handler_id", GUINT_TO_POINTER(handler_id));

    zathura_document_set_adjust_mode(document, ZATHURA_ADJUST_INPUTBAR);
    girara_dialog(zathura->ui.session, text, FALSE, NULL, callback, zathura->ui.session);
  }

  return false;
}

bool sc_abort(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
              unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura           = session->global.data;
  zathura_document_t* document = zathura_get_document(zathura);

  bool clear_search = true;
  girara_setting_get(session, "abort-clear-search", &clear_search);

  if (document != NULL) {
    const unsigned int number_of_pages = zathura_document_get_number_of_pages(document);
    for (unsigned int page_id = 0; page_id < number_of_pages; ++page_id) {
      zathura_page_t* page = zathura_document_get_page(document, page_id);
      if (page == NULL) {
        continue;
      }

      GtkWidget* page_widget   = zathura_page_get_widget(zathura, page);
      GObject* obj_page_widget = G_OBJECT(page_widget);
      zathura_page_widget_clear_selection(ZATHURA_PAGE(page_widget));
      g_object_set(obj_page_widget, "draw-links", FALSE, NULL);
      if (clear_search == true) {
        g_object_set(obj_page_widget, "draw-search-results", FALSE, NULL);
      }
    }
  }

  /* Setting the mode back here has not worked for ages. We need another way to
   * do this. Let's disable this for now.
   */
  /* girara_mode_set(session, session->modes.normal); */
  girara_sc_abort(session, NULL, NULL, 0);

  return false;
}

bool sc_adjust_window(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event),
                      unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);

  if (argument->n < ZATHURA_ADJUST_NONE || argument->n >= ZATHURA_ADJUST_MODE_NUMBER) {
    girara_error("Invalid adjust mode: %d", argument->n);
    girara_notify(session, GIRARA_ERROR, _("Invalid adjust mode: %d"), argument->n);
  } else {
    girara_debug("Setting adjust mode to: %d", argument->n);

    zathura_document_set_adjust_mode(zathura_get_document(zathura), argument->n);
    adjust_view(zathura);
  }

  return false;
}

bool sc_change_mode(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event),
                    unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);

  girara_mode_set(session, argument->n);

  return false;
}

bool sc_cycle_first_column(girara_session_t* session, girara_argument_t* UNUSED(argument),
                           girara_event_t* UNUSED(event), unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura           = session->global.data;
  zathura_document_t* document = zathura_get_document(zathura);

  if (document == NULL) {
    girara_notify(session, GIRARA_WARNING, _("No document opened."));
    return false;
  }

  int pages_per_row = 1;
  girara_setting_get(session, "pages-per-row", &pages_per_row);
  char* first_page_column_list = NULL;
  girara_setting_get(session, "first-page-column", &first_page_column_list);

  if (t == 0)
    t = 1;
  char* new_column_list = increment_first_page_column(first_page_column_list, pages_per_row, t);
  g_free(first_page_column_list);

  girara_setting_set(session, "first-page-column", new_column_list);
  g_free(new_column_list);

  return true;
}

bool sc_display_link(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
                     unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  return link_shortcuts(zathura, cb_sc_display_link, "Display Link: ");
}

bool sc_copy_link(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
                  unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  return link_shortcuts(zathura, cb_sc_copy_link, "Copy Link: ");
}

bool sc_copy_filepath(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
                      unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  GdkAtom* selection = get_selection(zathura);
  if (selection == NULL) {
    return false;
  }

  const char* file_path = zathura_document_get_path(zathura->document);
  if (file_path == NULL) {
    girara_debug("Could not get file path for copying");
    return false;
  }

  girara_debug("Copying file path to clipboard");
  gtk_clipboard_set_text(gtk_clipboard_get(*selection), file_path, -1);

  g_free(selection);
  return true;
}

bool sc_focus_inputbar(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event),
                       unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->gtk.inputbar_entry != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);

  zathura_document_set_adjust_mode(zathura->document, ZATHURA_ADJUST_INPUTBAR);

  if (gtk_widget_get_visible(GTK_WIDGET(session->gtk.inputbar)) == false) {
    gtk_widget_show(GTK_WIDGET(session->gtk.inputbar));
  }

  if (gtk_widget_get_visible(GTK_WIDGET(session->gtk.notification_area)) == true) {
    gtk_widget_hide(GTK_WIDGET(session->gtk.notification_area));
  }

  gtk_widget_grab_focus(GTK_WIDGET(session->gtk.inputbar_entry));

  if (argument->data != NULL) {
    gtk_entry_set_text(session->gtk.inputbar_entry, (char*)argument->data);

    /* append filepath */
    if (argument->n == APPEND_FILEPATH && zathura->document != NULL) {
      const char* file_path = zathura_document_get_path(zathura->document);
      if (file_path == NULL) {
        return false;
      }

      char* path    = g_path_get_dirname(file_path);
      char* escaped = girara_escape_string(path);
      char* tmp     = g_strdup_printf("%s%s/", (char*)argument->data, (g_strcmp0(path, "/") == 0) ? "" : escaped);
      g_free(path);
      g_free(escaped);

      gtk_entry_set_text(session->gtk.inputbar_entry, tmp);
      g_free(tmp);
    }

    GdkAtom* selection = get_selection(zathura);

    /* we save the X clipboard that will be clear by "grab_focus" */
    gchar* x_clipboard_text = NULL;

    if (selection != NULL) {
      x_clipboard_text = gtk_clipboard_wait_for_text(gtk_clipboard_get(*selection));
    }

    gtk_editable_set_position(GTK_EDITABLE(session->gtk.inputbar_entry), -1);

    if (x_clipboard_text != NULL && selection != NULL) {
      /* we reset the X clipboard with saved text */
      gtk_clipboard_set_text(gtk_clipboard_get(*selection), x_clipboard_text, -1);
      g_free(x_clipboard_text);
    }

    g_free(selection);
  }

  return true;
}

bool sc_follow(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
               unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  return link_shortcuts(zathura, cb_sc_follow, "Follow Link: ");
}

bool sc_goto(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event), unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);
  g_return_val_if_fail(zathura->document != NULL, false);

  zathura_jumplist_add(zathura);
  if (t != 0) {
    /* add offset */
    t += zathura_document_get_page_offset(zathura->document);

    page_set(zathura, t - 1);
  } else if (argument->n == TOP) {
    page_set(zathura, 0);
  } else if (argument->n == BOTTOM) {
    page_set(zathura, zathura_document_get_number_of_pages(zathura->document) - 1);
  }

  zathura_jumplist_add(zathura);

  return false;
}

bool sc_mouse_scroll(girara_session_t* session, girara_argument_t* argument, girara_event_t* event, unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);
  g_return_val_if_fail(event != NULL, false);

  if (zathura->document == NULL) {
    return false;
  }

  GtkAdjustment* x_adj = NULL;
  GtkAdjustment* y_adj = NULL;

  switch (event->type) {
    /* scroll */
  case GIRARA_EVENT_SCROLL_UP:
  case GIRARA_EVENT_SCROLL_DOWN:
  case GIRARA_EVENT_SCROLL_LEFT:
  case GIRARA_EVENT_SCROLL_RIGHT:
  case GIRARA_EVENT_SCROLL_BIDIRECTIONAL:
    return sc_scroll(session, argument, event, t);

    /* drag */
  case GIRARA_EVENT_BUTTON_PRESS:
    zathura->shortcut.mouse.x = event->x;
    zathura->shortcut.mouse.y = event->y;
    break;
  case GIRARA_EVENT_BUTTON_RELEASE:
    zathura->shortcut.mouse.x = 0;
    zathura->shortcut.mouse.y = 0;
    break;
  case GIRARA_EVENT_MOTION_NOTIFY:
    x_adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(zathura->ui.view));
    y_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(zathura->ui.view));

    if (x_adj == NULL || y_adj == NULL) {
      return false;
    }

    zathura_adjustment_set_value(x_adj, gtk_adjustment_get_value(x_adj) - (event->x - zathura->shortcut.mouse.x));
    zathura_adjustment_set_value(y_adj, gtk_adjustment_get_value(y_adj) - (event->y - zathura->shortcut.mouse.y));
    break;

    /* unhandled events */
  default:
    break;
  }

  return false;
}

bool sc_mouse_zoom(girara_session_t* session, girara_argument_t* argument, girara_event_t* event, unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);
  g_return_val_if_fail(event != NULL, false);

  if (zathura->document == NULL) {
    return false;
  }

  /* scroll event */
  switch (event->type) {
  case GIRARA_EVENT_SCROLL_UP:
    argument->n = ZOOM_IN;
    break;
  case GIRARA_EVENT_SCROLL_DOWN:
    argument->n = ZOOM_OUT;
    break;
  case GIRARA_EVENT_SCROLL_BIDIRECTIONAL:
    argument->n = ZOOM_SMOOTH;
    break;
  default:
    return false;
  }

  return sc_zoom(session, argument, event, t);
}

bool sc_navigate(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event),
                 unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);
  g_return_val_if_fail(zathura->document != NULL, false);

  int number_of_pages = zathura_document_get_number_of_pages(zathura->document);
  int new_page        = zathura_document_get_current_page_number(zathura->document);

  bool scroll_wrap = false;
  girara_setting_get(session, "scroll-wrap", &scroll_wrap);

  bool columns_per_row_offset = false;
  girara_setting_get(session, "advance-pages-per-row", &columns_per_row_offset);

  int offset = 1;
  if (columns_per_row_offset == true) {
    girara_setting_get(session, "pages-per-row", &offset);
  }

  t = (t == 0) ? (unsigned int)offset : t;
  if (argument->n == NEXT) {
    if (scroll_wrap == false) {
      new_page = new_page + t;
    } else {
      new_page = (new_page + t) % number_of_pages;
    }
  } else if (argument->n == PREVIOUS) {
    if (scroll_wrap == false) {
      new_page = new_page - t;
    } else {
      new_page = (new_page + number_of_pages - t) % number_of_pages;
    }
  }

  if (!scroll_wrap) {
    if (new_page <= 0) {
      new_page = 0;
    } else if (new_page >= number_of_pages) {
      new_page = number_of_pages - 1;
    }
  }

  page_set(zathura, new_page);

  return false;
}

bool sc_print(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
              unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  if (zathura->document == NULL) {
    girara_notify(session, GIRARA_ERROR, _("No document opened."));
    return false;
  }

  print(zathura);

  return true;
}

bool sc_recolor(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
                unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);

  bool value = false;
  girara_setting_get(session, "recolor", &value);
  value = !value;
  girara_setting_set(session, "recolor", &value);

  return false;
}

bool sc_reload(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
               unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  if (zathura->file_monitor.monitor == NULL) {
    return false;
  }

  /* Get file info (zoom, current page, etc.) */
  zathura_fileinfo_t file_info;
  if (zathura->document == NULL && zathura->predecessor_document != NULL) {
    /* Try to get the info from the predecessor document if the current does not exist */
    file_info = zathura_get_prefileinfo(zathura);
  } else {
    file_info = zathura_get_fileinfo(zathura);
  }

  /* close current document */
  document_close(zathura, true);

  /* reopen document with old file info */
  document_open(zathura, zathura_filemonitor_get_filepath(zathura->file_monitor.monitor), NULL,
                zathura->file_monitor.password, file_info.current_page, &file_info);

  return false;
}

bool sc_rotate(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event), unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(zathura->document != NULL, false);

  const unsigned int page_number = zathura_document_get_current_page_number(zathura->document);

  int angle = 90;
  if (argument != NULL && argument->n == ROTATE_CCW) {
    angle = 270;
  }

  /* update rotate value */
  t                     = (t == 0) ? 1 : t;
  unsigned int rotation = zathura_document_get_rotation(zathura->document);
  zathura_document_set_rotation(zathura->document, (rotation + angle * t) % 360);

  /* update scale */
  girara_argument_t new_argument = {.n = zathura_document_get_adjust_mode(zathura->document), .data = NULL};
  sc_adjust_window(zathura->ui.session, &new_argument, NULL, 0);

  /* render all pages again */
  render_all(zathura);

  page_set(zathura, page_number);

  return false;
}

bool sc_scroll(girara_session_t* session, girara_argument_t* argument, girara_event_t* event, unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);
  if (zathura->document == NULL) {
    return false;
  }

  /* if TOP or BOTTOM, go there and we are done */
  if (argument->n == TOP) {
    zathura_jumplist_add(zathura);
    position_set(zathura, -1, 0);
    zathura_jumplist_add(zathura);
    return false;
  } else if (argument->n == BOTTOM) {
    zathura_jumplist_add(zathura);
    position_set(zathura, -1, 1.0);
    zathura_jumplist_add(zathura);
    return false;
  }

  /* Retrieve current page and position */
  const unsigned int page_id = zathura_document_get_current_page_number(zathura->document);
  double pos_x               = zathura_document_get_position_x(zathura->document);
  double pos_y               = zathura_document_get_position_y(zathura->document);

  /* If PAGE_TOP or PAGE_BOTTOM, go there and we are done */
  if (argument->n == PAGE_TOP) {
    double dontcare = 0.5;
    page_number_to_position(zathura, page_id, dontcare, 0.0, &dontcare, &pos_y);
    position_set(zathura, pos_x, pos_y);
    return false;
  } else if (argument->n == PAGE_BOTTOM) {
    double dontcare = 0.5;
    page_number_to_position(zathura, page_id, dontcare, 1.0, &dontcare, &pos_y);
    position_set(zathura, pos_x, pos_y);
    return false;
  }

  if (t == 0) {
    t = 1;
  }

  unsigned int view_width  = 0;
  unsigned int view_height = 0;
  zathura_document_get_viewport_size(zathura->document, &view_height, &view_width);

  unsigned int doc_width  = 0;
  unsigned int doc_height = 0;
  zathura_document_widget_get_document_size(ZATHURA_DOCUMENT_WIDGET(zathura->ui.document_widget), &doc_height,
                                            &doc_width);

  float scroll_step = 40;
  girara_setting_get(session, "scroll-step", &scroll_step);
  float scroll_hstep = -1;
  girara_setting_get(session, "scroll-hstep", &scroll_hstep);
  if (scroll_hstep < 0) {
    scroll_hstep = scroll_step;
  }
  float scroll_full_overlap = 0.0;
  girara_setting_get(session, "scroll-full-overlap", &scroll_full_overlap);
  bool scroll_page_aware = false;
  girara_setting_get(session, "scroll-page-aware", &scroll_page_aware);

  bool scroll_wrap = false;
  girara_setting_get(session, "scroll-wrap", &scroll_wrap);

  /* compute the direction of scrolling */
  double direction = 1.0;
  if ((argument->n == LEFT) || (argument->n == FULL_LEFT) || (argument->n == HALF_LEFT) || (argument->n == UP) ||
      (argument->n == FULL_UP) || (argument->n == HALF_UP) || (argument->n == PARTIAL_UP)) {
    direction = -1.0;
  }

  const unsigned int v_padding = zathura_document_get_page_v_padding(zathura->document);
  const unsigned int h_padding = zathura_document_get_page_h_padding(zathura->document);

  const double vstep = (double)(view_height + v_padding) / (double)doc_height;
  const double hstep = (double)(view_width + h_padding) / (double)doc_width;

  /* compute new position */
  switch (argument->n) {
  case FULL_UP:
  case FULL_DOWN:
    pos_y += direction * (1.0 - scroll_full_overlap) * vstep;
    break;

  case FULL_LEFT:
  case FULL_RIGHT:
    pos_x += direction * (1.0 - scroll_full_overlap) * hstep;
    break;

  case HALF_UP:
  case HALF_DOWN:
    pos_y += direction * 0.5 * vstep;
    break;

  case PARTIAL_UP:
  case PARTIAL_DOWN:
    pos_y += direction * t * (scroll_step * 0.2) / (double)doc_height;
    break;

  case HALF_LEFT:
  case HALF_RIGHT:
    pos_x += direction * 0.5 * hstep;
    break;

  case UP:
  case DOWN:
    pos_y += direction * t * scroll_step / (double)doc_height;
    break;

  case LEFT:
  case RIGHT:
    pos_x += direction * t * scroll_hstep / (double)doc_width;
    break;

  case BIDIRECTIONAL: {
    pos_x += event->x * t * scroll_hstep / (double)doc_width;
    pos_y += event->y * t * scroll_step / (double)doc_height;
    break;
  }
  }

  /* handle boundaries */
  const double end_x = 0.5 * (double)view_width / (double)doc_width;
  const double end_y = 0.5 * (double)view_height / (double)doc_height;

  const double new_x = scroll_wrap ? 1.0 - end_x : end_x;
  const double new_y = scroll_wrap ? 1.0 - end_y : end_y;

  /* NOTE: the following `+ DBL_EPSILON` is added to avoid rounding errors, which can result in
   *       unwanted changes of page when positioned precisely in the middle of a dual-pane layout */
  if (pos_x < end_x + DBL_EPSILON) {
    pos_x = new_x;
  } else if (pos_x > 1.0 - end_x) {
    pos_x = 1 - new_x;
  }

  if (pos_y < end_y) {
    pos_y = new_y;
  } else if (pos_y > 1.0 - end_y) {
    pos_y = 1 - new_y;
  }

  /* snap to the border if we change page */
  const unsigned int new_page_id = position_to_page_number(zathura, pos_x, pos_y);
  if (scroll_page_aware == true && page_id != new_page_id) {
    double dummy = 0.0;
    switch (argument->n) {
    case FULL_LEFT:
    case HALF_LEFT:
      page_number_to_position(zathura, new_page_id, 1.0, 0.0, &pos_x, &dummy);
      break;

    case FULL_RIGHT:
    case HALF_RIGHT:
      page_number_to_position(zathura, new_page_id, 0.0, 0.0, &pos_x, &dummy);
      break;

    case FULL_UP:
    case HALF_UP:
    case PARTIAL_UP:
      page_number_to_position(zathura, new_page_id, 0.0, 1.0, &dummy, &pos_y);
      break;

    case FULL_DOWN:
    case HALF_DOWN:
    case PARTIAL_DOWN:
      page_number_to_position(zathura, new_page_id, 0.0, 0.0, &dummy, &pos_y);
      break;
    }
  }

  position_set(zathura, pos_x, pos_y);
  return false;
}

bool sc_jumplist(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event),
                 unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);
  g_return_val_if_fail(zathura->document != NULL, false);

  /* if no jumps in the jumplist */
  if (zathura->jumplist.size == 0) {
    return true;
  }

  zathura_jump_t* jump         = NULL;
  zathura_jump_t* current_jump = zathura_jumplist_current(zathura);

  switch (argument->n) {
  case FORWARD:
    for (int n = (t == 0 ? 1 : t); n > 0; n--) {
      zathura_jumplist_forward(zathura);
    }
    break;
  case BACKWARD:
    for (int n = (t == 0 ? 1 : t); n > 0; n--) {
      zathura_jumplist_backward(zathura);
    }
    break;
  }
  jump = zathura_jumplist_current(zathura);

  if (jump != current_jump) {
    page_set(zathura, jump->page);
    position_set(zathura, jump->x, jump->y);
  }

  return false;
}

bool sc_bisect(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event), unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);
  g_return_val_if_fail(zathura->document != NULL, false);

  const unsigned int num_pages = zathura_document_get_number_of_pages(zathura->document);
  const unsigned int cur_page  = zathura_document_get_current_page_number(zathura->document);

  /* process arguments */
  int direction;
  if (t > 0 && t <= num_pages) {
    /* bisect between cur_page and t */
    t -= 1;
    if (t == cur_page) {
      /* nothing to do */
      return false;
    } else if (t > cur_page) {
      zathura->bisect.start = cur_page;
      zathura->bisect.end   = t;
      direction             = BACKWARD;
    } else {
      zathura->bisect.start = t;
      zathura->bisect.end   = cur_page;
      direction             = FORWARD;
    }
  } else {
    direction = argument->n;

    /* setup initial bisect range */
    zathura_jump_t* jump = zathura_jumplist_current(zathura);
    if (jump == NULL) {
      girara_debug("bisecting between first and last page because there are no jumps");
      zathura->bisect.start = 0;
      zathura->bisect.end   = num_pages - 1;
    } else if (jump->page != cur_page || jump->page != zathura->bisect.last_jump) {
      girara_debug("last jump doesn't match up, starting new bisecting");
      zathura->bisect.start = 0;
      zathura->bisect.end   = num_pages - 1;

      unsigned int prev_page;
      if (direction == FORWARD) {
        prev_page = num_pages - 1;
      } else {
        prev_page = 0;
      }

      /* check if we have previous jumps */
      if (zathura_jumplist_has_previous(zathura) == true) {
        zathura_jumplist_backward(zathura);
        jump = zathura_jumplist_current(zathura);
        if (jump != NULL) {
          prev_page = jump->page;
        }
        zathura_jumplist_forward(zathura);
      }

      zathura->bisect.start     = MIN(prev_page, cur_page);
      zathura->bisect.end       = MAX(prev_page, cur_page);
      zathura->bisect.last_jump = cur_page;
    }
  }

  girara_debug("bisecting between %d and %d, at %d", zathura->bisect.start, zathura->bisect.end, cur_page);
  if (zathura->bisect.start == zathura->bisect.end) {
    /* nothing to do */
    return false;
  }

  unsigned int next_page  = cur_page;
  unsigned int next_start = zathura->bisect.start;
  unsigned int next_end   = zathura->bisect.end;

  /* here we have next_start <= next_page <= next_end */

  /* bisect step */
  switch (direction) {
  case FORWARD:
    if (cur_page != zathura->bisect.end) {
      next_page = (cur_page + zathura->bisect.end) / 2;
      if (next_page == cur_page) {
        ++next_page;
      }
      next_start = cur_page;
    }
    break;

  case BACKWARD:
    if (cur_page != zathura->bisect.start) {
      next_page = (cur_page + zathura->bisect.start) / 2;
      if (next_page == cur_page) {
        --next_page;
      }
      next_end = cur_page;
    }
    break;
  }

  if (next_page == cur_page) {
    /* nothing to do */
    return false;
  }

  girara_debug("bisecting between %d and %d, jumping to %d", zathura->bisect.start, zathura->bisect.end, next_page);
  zathura->bisect.last_jump = next_page;
  zathura->bisect.start     = next_start;
  zathura->bisect.end       = next_end;

  zathura_jumplist_add(zathura);
  page_set(zathura, next_page);
  zathura_jumplist_add(zathura);

  return false;
}

bool sc_search(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event),
               unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  return search_document(zathura, argument, false);
}

bool sc_navigate_index(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event),
                       unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);
  g_return_val_if_fail(zathura->document != NULL, false);

  if (zathura->ui.index == NULL) {
    return false;
  }

  GtkTreeView* tree_view = gtk_container_get_children(GTK_CONTAINER(zathura->ui.index))->data;
  GtkTreePath* path;
  GtkTreePath* start_path;
  GtkTreePath* end_path;

  gtk_tree_view_get_cursor(tree_view, &path, NULL);
  if (path == NULL) {
    return false;
  }

  if (gtk_tree_view_get_visible_range(tree_view, &start_path, &end_path) != TRUE) {
    girara_error("Cannot get visible range for index");
    goto free_and_return;
  }

  GtkTreeModel* model = gtk_tree_view_get_model(tree_view);
  GtkTreeIter iter, child_iter, parent_iter;

  gboolean need_to_scroll = FALSE;

  switch (argument->n) {
  case TOP:
    /* go to the first node */
    gtk_tree_path_free(path);
    path = gtk_tree_path_new_first();
    break;
  case BOTTOM:
    /* go to the last visible node */
    gtk_tree_path_free(path);
    path = gtk_tree_path_new_from_indices(gtk_tree_model_iter_n_children(model, NULL) - 1, -1);
    gtk_tree_model_get_iter(model, &iter, path);
    while (gtk_tree_model_iter_has_child(model, &iter) == TRUE && gtk_tree_view_row_expanded(tree_view, path) == TRUE) {
      gtk_tree_path_append_index(path, gtk_tree_model_iter_n_children(model, &iter) - 1);
    }
    break;
  case UP:
    for (int n = (t == 0 ? 1 : t); n > 0; n--) {
      if (gtk_tree_path_prev(path)) {
        while (gtk_tree_view_row_expanded(tree_view, path)) {
          gtk_tree_model_get_iter(model, &iter, path);
          /* select last child */
          gtk_tree_model_iter_nth_child(model, &child_iter, &iter, gtk_tree_model_iter_n_children(model, &iter) - 1);
          gtk_tree_path_free(path);
          path = gtk_tree_model_get_path(model, &child_iter);
        }
        continue;
      }
      gtk_tree_model_get_iter(model, &iter, path);
      if (gtk_tree_model_iter_parent(model, &parent_iter, &iter)) {
        gtk_tree_path_free(path);
        path = gtk_tree_model_get_path(model, &parent_iter);
      } else {
        break;
      }
    }
    break;
  case DOWN:
    for (int n = (t == 0 ? 1 : t); n > 0; n--) {
      if (gtk_tree_view_row_expanded(tree_view, path)) {
        gtk_tree_path_down(path);
        continue;
      }

      gtk_tree_model_get_iter(model, &iter, path);
      if (gtk_tree_model_iter_next(model, &iter)) {
        gtk_tree_path_free(path);
        path = gtk_tree_model_get_path(model, &iter);
        continue;
      }

      gtk_tree_model_get_iter(model, &iter, path);
      while (gtk_tree_model_iter_parent(model, &parent_iter, &iter)) {
        iter = parent_iter;
        if (gtk_tree_model_iter_next(model, &parent_iter)) {
          gtk_tree_path_free(path);
          path = gtk_tree_model_get_path(model, &parent_iter);
          break;
        }
      }
    }
    break;
  case HALF_UP:
  case PARTIAL_UP:
    gtk_tree_path_free(path);
    path           = gtk_tree_path_copy(start_path);
    need_to_scroll = TRUE;
    break;
  case HALF_DOWN:
  case PARTIAL_DOWN:
    gtk_tree_path_free(path);
    path           = gtk_tree_path_copy(end_path);
    need_to_scroll = TRUE;
    break;
  case EXPAND:
    if (gtk_tree_view_expand_row(tree_view, path, FALSE)) {
      gtk_tree_path_down(path);
    }
    break;
  case EXPAND_RECURSIVE:
    if (gtk_tree_view_expand_row(tree_view, path, TRUE)) {
      gtk_tree_path_down(path);
    }
    break;
  case EXPAND_ALL:
    gtk_tree_view_expand_all(tree_view);
    need_to_scroll = TRUE;
    break;
  case COLLAPSE:
    if (gtk_tree_view_collapse_row(tree_view, path) == FALSE && gtk_tree_path_get_depth(path) > 1) {
      gtk_tree_path_up(path);
      gtk_tree_view_collapse_row(tree_view, path);
    }
    break;
  case COLLAPSE_RECURSIVE:
    while (gtk_tree_path_get_depth(path) > 1) {
      gtk_tree_path_up(path);
    }
    gtk_tree_view_collapse_row(tree_view, path);
    break;
  case COLLAPSE_ALL:
    gtk_tree_view_collapse_all(tree_view);
    while (gtk_tree_path_get_depth(path) > 1) {
      gtk_tree_path_up(path);
    }
    break;
  case TOGGLE:
    gtk_tree_model_get_iter(model, &iter, path);
    if (gtk_tree_model_iter_has_child(model, &iter) == TRUE) {
      if (gtk_tree_view_row_expanded(tree_view, path) == TRUE) {
        gtk_tree_view_collapse_row(tree_view, path);
      } else {
        gtk_tree_view_expand_row(tree_view, path, FALSE);
      }
    }
    break;
  case SELECT:
    cb_index_row_activated(tree_view, path, NULL, zathura);
    goto free_and_return;
  }

  gtk_tree_view_set_cursor(tree_view, path, NULL, FALSE);
  if (need_to_scroll == TRUE) {
    gtk_tree_view_scroll_to_cell(tree_view, path, NULL, TRUE, 0.5, 0.0);
  }

free_and_return:
  gtk_tree_path_free(path);
  gtk_tree_path_free(start_path);
  gtk_tree_path_free(end_path);

  return false;
}

bool sc_toggle_index(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
                     unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  if (zathura->document == NULL) {
    return false;
  }

  girara_tree_node_t* document_index = NULL;
  GtkWidget* treeview                = NULL;
  GtkTreeModel* model                = NULL;
  GtkCellRenderer* renderer          = NULL;
  GtkCellRenderer* renderer2         = NULL;

  if (zathura->ui.index == NULL) {
    /* create new index widget */
    zathura->ui.index = gtk_scrolled_window_new(NULL, NULL);

    if (zathura->ui.index == NULL) {
      goto error_ret;
    }

    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(zathura->ui.index), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

    /* create index */
    document_index = zathura_document_index_generate(zathura->document, NULL);
    if (document_index == NULL) {
      girara_notify(session, GIRARA_WARNING, _("This document does not contain any index"));
      goto error_free;
    }

    model = GTK_TREE_MODEL(gtk_tree_store_new(4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER));
    if (model == NULL) {
      goto error_free;
    }

    treeview = gtk_tree_view_new_with_model(model);
    if (treeview == NULL) {
      goto error_free;
    }

    gtk_style_context_add_class(gtk_widget_get_style_context(treeview), "indexmode");

    g_object_unref(model);

    renderer = gtk_cell_renderer_text_new();
    if (renderer == NULL) {
      goto error_free;
    }

    renderer2 = gtk_cell_renderer_text_new();
    if (renderer2 == NULL) {
      goto error_free;
    }

    document_index_build(session, model, NULL, document_index);
    girara_node_free(document_index);

    /* setup widget */
    gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(treeview), 0, "Title", renderer, "markup", 0, NULL);
    gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(treeview), 1, "Target", renderer2, "text", 1, NULL);
    gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(treeview), 2, "(alt)", renderer2, "text", 2, NULL);

    gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(treeview), FALSE);
    g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    g_object_set(G_OBJECT(gtk_tree_view_get_column(GTK_TREE_VIEW(treeview), 0)), "expand", TRUE, NULL);
    gtk_tree_view_column_set_alignment(gtk_tree_view_get_column(GTK_TREE_VIEW(treeview), 1), 1.0f);
    gtk_tree_view_set_search_equal_func(GTK_TREE_VIEW(treeview), search_equal_func_index, treeview, NULL);
    gtk_tree_view_set_enable_search(GTK_TREE_VIEW(treeview), FALSE);
    g_signal_connect(G_OBJECT(treeview), "row-activated", G_CALLBACK(cb_index_row_activated), zathura);

    gtk_widget_set_visible(treeview, true);
    gtk_container_add(GTK_CONTAINER(zathura->ui.index), treeview);
  }

  if (girara_mode_get(session) == zathura->modes.index) {
    girara_set_view(zathura->ui.session, zathura->ui.view);
    girara_mode_set(zathura->ui.session, zathura->modes.normal);

    /* refresh view */
    refresh_view(zathura);
  } else {
    /* save current position to the jumplist */
    zathura_jumplist_add(zathura);

    const zathura_adjust_mode_t adjust_mode = zathura_document_get_adjust_mode(zathura->document);

    /* zathura goes to the first page when toggling index mode if this isn't done */
    if (adjust_mode == ZATHURA_ADJUST_INPUTBAR) {
      zathura_document_set_adjust_mode(zathura->document, ZATHURA_ADJUST_NONE);
    }

    girara_set_view(session, zathura->ui.index);
    // GtkTreeView* tree_view = gtk_container_get_children(GTK_CONTAINER(zathura->ui.index))->data;
    // gtk_widget_grab_focus(GTK_WIDGET(tree_view));
    index_scroll_to_current_page(zathura);
    girara_mode_set(zathura->ui.session, zathura->modes.index);
  }

  return false;

error_free:

  if (zathura->ui.index != NULL) {
    g_object_ref_sink(zathura->ui.index);
    zathura->ui.index = NULL;
  }

  if (document_index != NULL) {
    girara_node_free(document_index);
  }

error_ret:

  return false;
}

bool sc_toggle_page_mode(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
                         unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  if (zathura->document == NULL) {
    girara_notify(session, GIRARA_WARNING, _("No document opened."));
    return false;
  }

  unsigned int page_id = zathura_document_get_current_page_number(zathura->document);

  int pages_per_row = 1;
  girara_setting_get(zathura->ui.session, "pages-per-row", &pages_per_row);

  int value = 1;
  if (pages_per_row == 1) {
    value = zathura->shortcut.toggle_page_mode.pages;
  } else {
    zathura->shortcut.toggle_page_mode.pages = pages_per_row;
  }

  girara_setting_set(zathura->ui.session, "pages-per-row", &value);
  adjust_view(zathura);

  page_set(zathura, page_id);
  render_all(zathura);
  refresh_view(zathura);

  return true;
}

bool sc_toggle_fullscreen(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
                          unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  if (zathura->document == NULL) {
    girara_notify(session, GIRARA_WARNING, _("No document opened."));
    return false;
  }

  const girara_mode_t old_mode = girara_mode_get(session);
  if (old_mode == zathura->modes.fullscreen) {
    gtk_window_unfullscreen(GTK_WINDOW(session->gtk.window));
    refresh_view(zathura);
    girara_mode_set(session, zathura->modes.normal);
  } else if (old_mode == zathura->modes.normal) {
    gtk_window_fullscreen(GTK_WINDOW(session->gtk.window));
    refresh_view(zathura);
    girara_mode_set(session, zathura->modes.fullscreen);
  }

  return false;
}

bool sc_toggle_presentation(girara_session_t* session, girara_argument_t* UNUSED(argument),
                            girara_event_t* UNUSED(event), unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  if (zathura->document == NULL) {
    girara_notify(session, GIRARA_WARNING, _("No document opened."));
    return false;
  }

  const girara_mode_t old_mode = girara_mode_get(session);
  if (old_mode == zathura->modes.presentation) {
    /* reset pages per row */
    girara_setting_set(session, "pages-per-row", &zathura->shortcut.toggle_presentation_mode.pages);

    /* reset first page column */
    if (zathura->shortcut.toggle_presentation_mode.first_page_column_list != NULL) {
      girara_setting_set(session, "first-page-column",
                         zathura->shortcut.toggle_presentation_mode.first_page_column_list);
    }

    /* show status bar if it was enabled */
    if (zathura->shortcut.toggle_presentation_mode.is_status_bar_visible) {
      gtk_widget_show(GTK_WIDGET(session->gtk.statusbar));
    }
    /* show input bar if if was enabled */
    if (zathura->shortcut.toggle_presentation_mode.is_input_bar_visible) {
      gtk_widget_show(GTK_WIDGET(session->gtk.inputbar));
    }

    /* set full screen */
    gtk_window_unfullscreen(GTK_WINDOW(session->gtk.window));

    /* reset zoom */
    zathura_document_set_zoom(zathura->document, zathura->shortcut.toggle_presentation_mode.zoom);
    render_all(zathura);
    refresh_view(zathura);

    /* set mode */
    girara_mode_set(session, zathura->modes.normal);
  } else if (old_mode == zathura->modes.normal) {
    /* backup pages per row */
    girara_setting_get(session, "pages-per-row", &zathura->shortcut.toggle_presentation_mode.pages);

    /* backup first page column */
    g_free(zathura->shortcut.toggle_presentation_mode.first_page_column_list);
    zathura->shortcut.toggle_presentation_mode.first_page_column_list = NULL;
    /* this will leak. we need to move the values somewhere else */
    girara_setting_get(session, "first-page-column",
                       &zathura->shortcut.toggle_presentation_mode.first_page_column_list);

    /* set single view */
    int int_value = 1;
    girara_setting_set(session, "pages-per-row", &int_value);

    /* back up zoom */
    zathura->shortcut.toggle_presentation_mode.zoom = zathura_document_get_zoom(zathura->document);

    /* adjust window */
    girara_argument_t argument = {.n = ZATHURA_ADJUST_BESTFIT, .data = NULL};
    sc_adjust_window(session, &argument, NULL, 0);

    zathura->shortcut.toggle_presentation_mode.is_status_bar_visible =
        gtk_widget_get_visible(GTK_WIDGET(session->gtk.statusbar));
    zathura->shortcut.toggle_presentation_mode.is_input_bar_visible =
        gtk_widget_get_visible(GTK_WIDGET(session->gtk.inputbar));

    /* hide status and inputbar */
    gtk_widget_hide(GTK_WIDGET(session->gtk.inputbar));
    gtk_widget_hide(GTK_WIDGET(session->gtk.statusbar));

    /* set full screen */
    gtk_window_fullscreen(GTK_WINDOW(session->gtk.window));
    refresh_view(zathura);

    /* set mode */
    girara_mode_set(session, zathura->modes.presentation);
  }

  return false;
}

bool sc_quit(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
             unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);

  girara_argument_t arg = {.n = GIRARA_HIDE, .data = NULL};
  girara_isc_completion(session, &arg, NULL, 0);

  cb_destroy(NULL, NULL);

  return false;
}

bool sc_zoom(girara_session_t* session, girara_argument_t* argument, girara_event_t* event, unsigned int t) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(argument != NULL, false);
  g_return_val_if_fail(zathura->document != NULL, false);

  zathura_document_set_adjust_mode(zathura->document, ZATHURA_ADJUST_NONE);

  /* retrieve zoom step value */
  int value = 1;
  girara_setting_get(zathura->ui.session, "zoom-step", &value);

  const int nt           = (t == 0) ? 1 : t;
  const double zoom_step = 1.0 + value / 100.0 * nt;
  const double old_zoom  = zathura_document_get_zoom(zathura->document);

  /* specify new zoom value */
  if (argument->n == ZOOM_IN) {
    girara_debug("Increasing zoom by %0.2f.", zoom_step - 1.0);
    zathura_document_set_zoom(zathura->document, old_zoom * zoom_step);
  } else if (argument->n == ZOOM_OUT) {
    girara_debug("Decreasing zoom by %0.2f.", zoom_step - 1.0);
    zathura_document_set_zoom(zathura->document, old_zoom / zoom_step);
  } else if (argument->n == ZOOM_SPECIFIC) {
    if (t == 0) {
      girara_debug("Setting zoom to 1.");
      zathura_document_set_zoom(zathura->document, 1.0);
    } else {
      girara_debug("Setting zoom to %0.2f.", t / 100.0);
      zathura_document_set_zoom(zathura->document, t / 100.0);
    }
  } else if (argument->n == ZOOM_SMOOTH) {
    const double dy = (event != NULL) ? event->y : 1.0;
    const double z  = pow(zoom_step, -dy);
    girara_debug("Increasing zoom by %0.2f.", z - 1.0);
    zathura_document_set_zoom(zathura->document, old_zoom * z);
  } else {
    girara_debug("Setting zoom to 1.");
    zathura_document_set_zoom(zathura->document, 1.0);
  }

  /* zoom limitations */
  const double zoom = zathura_document_get_zoom(zathura->document);
  zathura_document_set_zoom(zathura->document, zathura_correct_zoom_value(session, zoom));

  const double new_zoom = zathura_document_get_zoom(zathura->document);
  if (fabs(new_zoom - old_zoom) <= DBL_EPSILON) {
    girara_debug("New and old zoom level are too close: %0.2f vs. %0.2f", new_zoom, old_zoom);
    return false;
  }

  girara_debug("Re-rendering with new zoom level %0.2f.", new_zoom);
  render_all(zathura);
  refresh_view(zathura);

  return false;
}

static bool sc_exec_internal(girara_session_t* session, girara_argument_t* argument) {
  if (argument == NULL || argument->data == NULL) {
    return false;
  }

  /* create argument list */
  g_autoptr(girara_list_t) argument_list = argument_to_argument_list(argument);
  if (argument_list == NULL) {
    return false;
  }

  /* call exec */
  cmd_exec(session, argument_list);

  return false;
}

bool sc_exec(girara_session_t* session, girara_argument_t* argument, girara_event_t* UNUSED(event),
             unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  if (argument == NULL || argument->data == NULL) {
    return false;
  }

  girara_argument_t new_argument = *argument;
  const char* bus_name           = zathura_dbus_get_name(zathura);
  char* s                        = girara_replace_substring(new_argument.data, "$DBUS", bus_name);
  if (s == NULL) {
    return false;
  }
  new_argument.data = s;

  if (zathura->document != NULL) {
    const char* path  = zathura_document_get_path(zathura->document);
    unsigned int page = zathura_document_get_current_page_number(zathura->document);
    char page_buf[G_ASCII_DTOSTR_BUF_SIZE];
    g_ascii_dtostr(page_buf, G_ASCII_DTOSTR_BUF_SIZE, page + 1);

    s = girara_replace_substring(new_argument.data, "$FILE", path);
    g_free(new_argument.data);

    if (s == NULL) {
      return false;
    }
    new_argument.data = s;

    s = girara_replace_substring(new_argument.data, "$PAGE", page_buf);
    g_free(new_argument.data);

    if (s == NULL) {
      return false;
    }
    new_argument.data = s;
  }

  const bool ret = sc_exec_internal(session, &new_argument);
  g_free(new_argument.data);
  return ret;
}

bool sc_nohlsearch(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
                   unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  document_draw_search_results(zathura, false);
  render_all(zathura);

  return false;
}

bool sc_snap_to_page(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
                     unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;
  g_return_val_if_fail(zathura->document != NULL, false);
  zathura_document_t* document = zathura->document;

  int page = zathura_document_get_current_page_number(document);
  return page_set(zathura, page);
}

bool sc_file_chooser(girara_session_t* session, girara_argument_t* UNUSED(argument), girara_event_t* UNUSED(event),
                     unsigned int UNUSED(t)) {
  g_return_val_if_fail(session != NULL, false);
  g_return_val_if_fail(session->global.data != NULL, false);
  zathura_t* zathura = session->global.data;

  GtkFileChooserNative* native =
      gtk_file_chooser_native_new("Open File", NULL, GTK_FILE_CHOOSER_ACTION_OPEN, "_Open", "_Cancel");
  GtkFileChooser* chooser = GTK_FILE_CHOOSER(native);

  GtkFileFilter* filter     = gtk_file_filter_new();
  girara_list_t* mime_types = zathura_plugin_manager_get_content_types(zathura->plugins.manager);
  if (mime_types) {
    for (size_t idx = 0; idx != girara_list_size(mime_types); ++idx) {
      const char* mime_type = girara_list_nth(mime_types, idx);
      girara_debug("adding mime type to %s to filter", mime_type);
      gtk_file_filter_add_mime_type(filter, mime_type);
    }
    gtk_file_chooser_add_filter(chooser, filter);
  } else {
    g_object_unref(filter);
  }

  const gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
  if (res == GTK_RESPONSE_ACCEPT) {
    char* filename = gtk_file_chooser_get_filename(chooser);
    if (zathura_has_document(zathura) && !document_close(zathura, false)) {
      g_free(filename);
      goto error;
    }

    if (!document_open(zathura, filename, NULL, NULL, 0, NULL)) {
      g_free(filename);
      goto error;
    }

    g_free(filename);
  }

  g_object_unref(native);
  return true;

error:

  g_object_unref(native);
  return false;
}
