guake-guake/guake/guake_app.py
Gaetan Semet 3ac1a69660 Replace autopep8, yapf by black
Signed-off-by: Gaetan Semet <gaetan@xeberon.net>
2020-02-18 22:58:54 +01:00

1474 lines
56 KiB
Python

# -*- coding: utf-8; -*-
"""
Copyright (C) 2007-2012 Lincoln de Sousa <lincoln@minaslivre.org>
Copyright (C) 2007 Gabriel Falcão <gabrielteratos@gmail.com>
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 2 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, write to the
Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301 USA
"""
import json
import logging
import os
import platform
import shutil
import subprocess
import sys
import time as pytime
import traceback
import uuid
from pathlib import Path
from urllib.parse import quote_plus
from xml.sax.saxutils import escape as xml_escape
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
gi.require_version("Vte", "2.91") # vte-0.42
gi.require_version("Keybinder", "3.0")
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import GdkX11
from gi.repository import Gio
from gi.repository import Gtk
from gi.repository import Keybinder
from gi.repository import Vte
import cairo
from guake import gtk_version
from guake import guake_version
from guake import notifier
from guake import vte_version
from guake.about import AboutDialog
from guake.common import gladefile
from guake.common import pixmapfile
from guake.dialogs import PromptQuitDialog
from guake.globals import ALIGN_BOTTOM
from guake.globals import ALIGN_CENTER
from guake.globals import ALIGN_LEFT
from guake.globals import ALIGN_RIGHT
from guake.globals import ALIGN_TOP
from guake.globals import ALWAYS_ON_PRIMARY
from guake.globals import MAX_TRANSPARENCY
from guake.globals import NAME
from guake.globals import TABS_SESSION_SCHEMA_VERSION
from guake.gsettings import GSettingHandler
from guake.guake_logging import setupLogging
from guake.keybindings import Keybindings
from guake.notebook import NotebookManager
from guake.notebook import TerminalNotebook
from guake.palettes import PALETTES
from guake.paths import LOCALE_DIR
from guake.paths import SCHEMA_DIR
from guake.paths import try_to_compile_glib_schemas
from guake.prefs import PrefsDialog
from guake.prefs import refresh_user_start
from guake.settings import Settings
from guake.simplegladeapp import SimpleGladeApp
from guake.terminal import GuakeTerminal
from guake.theme import patch_gtk_theme
from guake.theme import select_gtk_theme
from guake.utils import FullscreenManager
from guake.utils import HidePrevention
from guake.utils import RectCalculator
from guake.utils import TabNameUtils
from guake.utils import get_server_time
from guake.utils import save_tabs_when_changed
from locale import gettext as _
log = logging.getLogger(__name__)
instance = None
RESPONSE_FORWARD = 0
RESPONSE_BACKWARD = 1
# Disable find feature until python-vte hasn't been updated
enable_find = False
GObject.threads_init()
# Setting gobject program name
GObject.set_prgname(NAME)
GDK_WINDOW_STATE_WITHDRAWN = 1
GDK_WINDOW_STATE_ICONIFIED = 2
GDK_WINDOW_STATE_STICKY = 8
GDK_WINDOW_STATE_ABOVE = 32
class Guake(SimpleGladeApp):
"""Guake main class. Handles specialy the main window.
"""
def __init__(self):
def load_schema():
log.info("Loading Gnome schema from: %s", SCHEMA_DIR)
return Gio.SettingsSchemaSource.new_from_directory(
SCHEMA_DIR, Gio.SettingsSchemaSource.get_default(), False
)
try:
schema_source = load_schema()
except GLib.Error: # pylint: disable=catching-non-exception
log.exception("Unable to load the GLib schema, try to compile it")
try_to_compile_glib_schemas()
schema_source = load_schema()
self.settings = Settings(schema_source)
log.info("Language previously loaded from: %s", LOCALE_DIR)
super(Guake, self).__init__(gladefile("guake.glade"))
select_gtk_theme(self.settings)
patch_gtk_theme(self.get_widget("window-root").get_style_context(), self.settings)
self.add_callbacks(self)
log.info("Guake Terminal %s", guake_version())
log.info("VTE %s", vte_version())
log.info("Gtk %s", gtk_version())
self.hidden = True
self.forceHide = False
# trayicon! Using SVG handles better different OS trays
# img = pixmapfile('guake-tray.svg')
# trayicon!
img = pixmapfile("guake-tray.png")
try:
import appindicator # pylint: disable=import-outside-toplevel
except ImportError:
self.tray_icon = Gtk.StatusIcon()
self.tray_icon.set_from_file(img)
self.tray_icon.set_tooltip_text(_("Guake Terminal"))
self.tray_icon.connect("popup-menu", self.show_menu)
self.tray_icon.connect("activate", self.show_hide)
else:
# TODO PORT test this on a system with app indicator
self.tray_icon = appindicator.Indicator(
_("guake-indicator"), _("guake-tray"), appindicator.CATEGORY_OTHER
)
self.tray_icon.set_icon(img)
self.tray_icon.set_status(appindicator.STATUS_ACTIVE)
menu = self.get_widget("tray-menu")
show = Gtk.MenuItem(_("Show"))
show.set_sensitive(True)
show.connect("activate", self.show_hide)
show.show()
menu.prepend(show)
self.tray_icon.set_menu(menu)
# important widgets
self.window = self.get_widget("window-root")
self.window.set_name("guake-terminal")
self.window.set_keep_above(True)
self.mainframe = self.get_widget("mainframe")
self.mainframe.remove(self.get_widget("notebook-teminals"))
# Pending restore for terminal split after show-up
# [(RootTerminalBox, TerminaBox, panes), ...]
self.pending_restore_page_split = []
self._failed_restore_page_split = []
# FullscreenManager
self.fullscreen_manager = FullscreenManager(self.settings, self.window, self)
# Workspace tracking
self.notebook_manager = NotebookManager(
self.window,
self.mainframe,
self.settings.general.get_boolean("workspace-specific-tab-sets"),
self.terminal_spawned,
self.page_deleted,
)
self.notebook_manager.connect("notebook-created", self.notebook_created)
self.notebook_manager.set_workspace(0)
self.set_tab_position()
# check and set ARGB for real transparency
self.update_visual()
self.window.get_screen().connect("composited-changed", self.update_visual)
# Debounce accel_search_terminal
self.prev_accel_search_terminal_time = 0.0
# holds the timestamp of the losefocus event
self.losefocus_time = 0
# holds the timestamp of the previous show/hide action
self.prev_showhide_time = 0
# Controls the transparency state needed for function accel_toggle_transparency
self.transparency_toggled = False
# store the default window title to reset it when update is not wanted
self.default_window_title = self.window.get_title()
self.display_tab_names = 0
self.window.connect("focus-out-event", self.on_window_losefocus)
# Handling the delete-event of the main window to avoid
# problems when closing it.
def destroy(*args):
self.hide()
return True
def window_event(*args):
return self.window_event(*args)
self.window.connect("delete-event", destroy)
self.window.connect("window-state-event", window_event)
# this line is important to resize the main window and make it
# smaller.
# TODO PORT do we still need this?
# self.window.set_geometry_hints(min_width=1, min_height=1)
# special trick to avoid the "lost guake on Ubuntu 'Show Desktop'" problem.
# DOCK makes the window foundable after having being "lost" after "Show
# Desktop"
self.window.set_type_hint(Gdk.WindowTypeHint.DOCK)
# Restore back to normal behavior
self.window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
# loading and setting up configuration stuff
GSettingHandler(self)
Keybinder.init()
self.hotkeys = Keybinder
Keybindings(self)
self.load_config()
if self.settings.general.get_boolean("start-fullscreen"):
self.fullscreen()
refresh_user_start(self.settings)
# Restore tabs when startup
if self.settings.general.get_boolean("restore-tabs-startup"):
self.restore_tabs(suppress_notify=True)
# Pop-up that shows that guake is working properly (if not
# unset in the preferences windows)
if self.settings.general.get_boolean("use-popup"):
key = self.settings.keybindingsGlobal.get_string("show-hide")
keyval, mask = Gtk.accelerator_parse(key)
label = Gtk.accelerator_get_label(keyval, mask)
filename = pixmapfile("guake-notification.png")
notifier.showMessage(
_("Guake Terminal"),
_("Guake is now running,\n" "press <b>{!s}</b> to use it.").format(
xml_escape(label)
),
filename,
)
log.info("Guake initialized")
def get_notebook(self):
return self.notebook_manager.get_current_notebook()
def notebook_created(self, nm, notebook, key):
notebook.attach_guake(self)
# Tracking when reorder page
notebook.connect("page-reordered", self.on_page_reorder)
def update_visual(self, user_data=None):
screen = self.window.get_screen()
visual = screen.get_rgba_visual()
if visual and screen.is_composited():
# NOTE: We should re-realize window when update window visual
# Otherwise it may failed, when the Guake it start without compositor
self.window.unrealize()
self.window.set_visual(visual)
self.window.set_app_paintable(True)
self.window.transparency = True
self.window.realize()
if self.window.get_property("visible"):
self.hide()
self.show()
else:
log.warn("System doesn't support transparency")
self.window.transparency = False
self.window.set_visual(screen.get_system_visual())
# new color methods should be moved to the GuakeTerminal class
def _load_palette(self):
colorRGBA = Gdk.RGBA(0, 0, 0, 0)
paletteList = list()
for color in self.settings.styleFont.get_string("palette").split(":"):
colorRGBA.parse(color)
paletteList.append(colorRGBA.copy())
return paletteList
def _get_background_color(self, palette_list):
if len(palette_list) > 16:
bg_color = palette_list[17]
else:
bg_color = Gdk.RGBA(0, 0, 0, 0.9)
return self._apply_transparency_to_color(bg_color)
def _apply_transparency_to_color(self, bg_color):
transparency = self.settings.styleBackground.get_int("transparency")
if not self.transparency_toggled:
bg_color.alpha = 1 / 100 * transparency
else:
bg_color.alpha = 1
return bg_color
def set_background_color_from_settings(self, terminal_uuid=None):
self.set_colors_from_settings(terminal_uuid)
def get_bgcolor(self):
palette_list = self._load_palette()
return self._get_background_color(palette_list)
def get_fgcolor(self):
palette_list = self._load_palette()
if len(palette_list) > 16:
font_color = palette_list[16]
else:
font_color = Gdk.RGBA(0, 0, 0, 0)
return font_color
def set_colors_from_settings(self, terminal_uuid=None):
bg_color = self.get_bgcolor()
font_color = self.get_fgcolor()
palette_list = self._load_palette()
terminals = self.get_notebook().iter_terminals()
if terminal_uuid:
terminals = [t for t in terminals if t.uuid == terminal_uuid]
for i in terminals:
i.set_color_foreground(font_color)
i.set_color_bold(font_color)
i.set_colors(font_color, bg_color, palette_list[:16])
def set_colors_from_settings_on_page(self, current_terminal_only=False, page_num=None):
"""If page_num is None, sets colors on the current page."""
bg_color = self.get_bgcolor()
font_color = self.get_fgcolor()
palette_list = self._load_palette()
if current_terminal_only:
terminal = self.get_notebook().get_current_terminal()
terminal.set_color_foreground(font_color)
terminal.set_color_bold(font_color)
terminal.set_colors(font_color, bg_color, palette_list[:16])
else:
if page_num is None:
page_num = self.get_notebook().get_current_page()
for terminal in self.get_notebook().get_nth_page(page_num).iter_terminals():
terminal.set_color_foreground(font_color)
terminal.set_color_bold(font_color)
terminal.set_colors(font_color, bg_color, palette_list[:16])
def reset_terminal_custom_colors(
self, current_terminal=False, current_page=False, terminal_uuid=None
):
"""Resets terminal(s) colors to the settings colors.
If current_terminal == False and current_page == False and terminal_uuid is None,
resets colors of all terminals.
"""
terminals = []
if current_terminal:
terminals.append(self.get_notebook().get_current_terminal())
if current_page:
page_num = self.get_notebook().get_current_page()
for t in self.get_notebook().get_nth_page(page_num).iter_terminals():
terminals.append(t)
if terminal_uuid:
for t in self.get_notebook().iter_terminals():
if t.uuid == terminal_uuid:
terminals.append(t)
if not current_terminal and not current_page and not terminal_uuid:
terminals = list(self.get_notebook().iter_terminals())
for i in terminals:
i.reset_custom_colors()
def set_bgcolor(self, bgcolor, current_terminal_only=False):
if isinstance(bgcolor, str):
c = Gdk.RGBA(0, 0, 0, 0)
log.debug("Building Gdk Color from: %r", bgcolor)
c.parse("#" + bgcolor)
bgcolor = c
if not isinstance(bgcolor, Gdk.RGBA):
raise TypeError("color should be Gdk.RGBA, is: {!r}".format(bgcolor))
bgcolor = self._apply_transparency_to_color(bgcolor)
log.debug("setting background color to: %r", bgcolor)
if current_terminal_only:
self.get_notebook().get_current_terminal().set_color_background_custom(bgcolor)
else:
page_num = self.get_notebook().get_current_page()
for terminal in self.get_notebook().get_nth_page(page_num).iter_terminals():
terminal.set_color_background_custom(bgcolor)
def set_fgcolor(self, fgcolor, current_terminal_only=False):
if isinstance(fgcolor, str):
c = Gdk.RGBA(0, 0, 0, 0)
log.debug("Building Gdk Color from: %r", fgcolor)
c.parse("#" + fgcolor)
fgcolor = c
if not isinstance(fgcolor, Gdk.RGBA):
raise TypeError("color should be Gdk.RGBA, is: {!r}".format(fgcolor))
log.debug("setting background color to: %r", fgcolor)
if current_terminal_only:
self.get_notebook().get_current_terminal().set_color_foreground_custom(fgcolor)
else:
page_num = self.get_notebook().get_current_page()
for terminal in self.get_notebook().get_nth_page(page_num).iter_terminals():
terminal.set_color_foreground_custom(fgcolor)
def change_palette_name(self, palette_name):
if isinstance(palette_name, str):
if palette_name not in PALETTES:
log.info("Palette name %s not found", palette_name)
return
log.debug("Settings palette name to %s", palette_name)
self.settings.styleFont.set_string("palette", PALETTES[palette_name])
self.settings.styleFont.set_string("palette-name", palette_name)
self.set_colors_from_settings()
def execute_command(self, command, tab=None):
# TODO DBUS_ONLY
"""Execute the `command' in the `tab'. If tab is None, the
command will be executed in the currently selected
tab. Command should end with '\n', otherwise it will be
appended to the string.
"""
# TODO CONTEXTMENU this has to be rewriten and only serves the
# dbus interface, maybe this should be moved to dbusinterface.py
if not self.get_notebook().has_page():
self.add_tab()
if command[-1] != "\n":
command += "\n"
terminal = self.get_notebook().get_current_terminal()
terminal.feed_child(command)
def execute_command_by_uuid(self, tab_uuid, command):
# TODO DBUS_ONLY
"""Execute the `command' in the tab whose terminal has the `tab_uuid' uuid
"""
if command[-1] != "\n":
command += "\n"
try:
tab_uuid = uuid.UUID(tab_uuid)
(page_index,) = (
index
for index, t in enumerate(self.get_notebook().iter_terminals())
if t.get_uuid() == tab_uuid
)
except ValueError:
pass
else:
terminals = self.get_notebook().get_terminals_for_page(page_index)
for current_vte in terminals:
current_vte.feed_child(command)
def on_window_losefocus(self, window, event):
"""Hides terminal main window when it loses the focus and if
the window_losefocus gconf variable is True.
"""
if not HidePrevention(self.window).may_hide():
return
value = self.settings.general.get_boolean("window-losefocus")
visible = window.get_property("visible")
self.losefocus_time = get_server_time(self.window)
if visible and value:
log.info("Hiding on focus lose")
self.hide()
def show_menu(self, status_icon, button, activate_time):
"""Show the tray icon menu.
"""
menu = self.get_widget("tray-menu")
menu.popup(None, None, None, Gtk.StatusIcon.position_menu, button, activate_time)
def show_about(self, *args):
# TODO DBUS ONLY
# TODO TRAY ONLY
"""Hides the main window and creates an instance of the About
Dialog.
"""
self.hide()
AboutDialog()
def show_prefs(self, *args):
# TODO DBUS ONLY
# TODO TRAY ONLY
"""Hides the main window and creates an instance of the
Preferences window.
"""
self.hide()
PrefsDialog(self.settings).show()
def is_iconified(self):
# TODO this is "dead" code only gets called to log output or in out commented code
if self.window:
cur_state = int(self.window.get_state())
return bool(cur_state & GDK_WINDOW_STATE_ICONIFIED)
return False
def window_event(self, window, event):
window_state = event.new_window_state
self.fullscreen_manager.set_window_state(window_state)
log.debug("Received window state event: %s", window_state)
def show_hide(self, *args):
"""Toggles the main window visibility
"""
log.debug("Show_hide called")
if self.forceHide:
self.forceHide = False
return
if not HidePrevention(self.window).may_hide():
return
if not self.win_prepare():
return
if not self.window.get_property("visible"):
log.info("Showing the terminal")
self.show()
self.set_terminal_focus()
return
# Disable the focus_if_open feature
# - if doesn't work seamlessly on all system
# - self.window.window.get_state doesn't provides us the right information on all
# systems, especially on MATE/XFCE
#
# if self.client.get_bool(KEY('/general/focus_if_open')):
# restore_focus = False
# if self.window.window:
# state = int(self.window.window.get_state())
# if ((state & GDK_WINDOW_STATE_STICKY or
# state & GDK_WINDOW_STATE_WITHDRAWN
# )):
# restore_focus = True
# else:
# restore_focus = True
# if not self.hidden:
# restore_focus = True
# if restore_focus:
# log.debug("DBG: Restoring the focus to the terminal")
# self.hide()
# self.show()
# self.window.window.focus()
# self.set_terminal_focus()
# return
log.info("Hiding the terminal")
self.hide()
def show_focus(self, *args):
self.win_prepare()
self.show()
self.set_terminal_focus()
def win_prepare(self, *args):
event_time = self.hotkeys.get_current_event_time()
if (
not self.settings.general.get_boolean("window-refocus")
and self.window.get_window()
and self.window.get_property("visible")
):
pass
elif not self.settings.general.get_boolean("window-losefocus"):
if self.losefocus_time and self.losefocus_time < event_time:
if (
self.window.get_window()
and self.window.get_property("visible")
and not self.window.get_window().get_state() & Gdk.WindowState.FOCUSED
):
log.debug("DBG: Restoring the focus to the terminal")
self.window.get_window().focus(event_time)
self.set_terminal_focus()
self.losefocus_time = 0
return False
elif self.losefocus_time and self.settings.general.get_boolean("window-losefocus"):
if self.losefocus_time >= event_time and (self.losefocus_time - event_time) < 10:
self.losefocus_time = 0
return False
# limit rate at which the visibility can be toggled.
if self.prev_showhide_time and event_time and (event_time - self.prev_showhide_time) < 65:
return False
self.prev_showhide_time = event_time
log.debug("")
log.debug("=" * 80)
log.debug("Window display")
if self.window:
cur_state = int(self.window.get_state())
is_sticky = bool(cur_state & GDK_WINDOW_STATE_STICKY)
is_withdrawn = bool(cur_state & GDK_WINDOW_STATE_WITHDRAWN)
is_above = bool(cur_state & GDK_WINDOW_STATE_ABOVE)
is_iconified = self.is_iconified()
log.debug("gtk.gdk.WindowState = %s", cur_state)
log.debug("GDK_WINDOW_STATE_STICKY? %s", is_sticky)
log.debug("GDK_WINDOW_STATE_WITHDRAWN? %s", is_withdrawn)
log.debug("GDK_WINDOW_STATE_ABOVE? %s", is_above)
log.debug("GDK_WINDOW_STATE_ICONIFIED? %s", is_iconified)
return True
return False
def restore_pending_terminal_split(self):
# Restore pending terminal split
# XXX: Consider refactor how to handle failed restore page split
self.pending_restore_page_split = self._failed_restore_page_split
self._failed_restore_page_split = []
for root, box, panes in self.pending_restore_page_split:
if (
self.window.get_property("visible")
and root.get_notebook() == self.notebook_manager.get_current_notebook()
):
root.restore_box_layout(box, panes)
else:
# Consider failed if the window is not visible
self._failed_restore_page_split.append((root, box, panes))
def show(self):
"""Shows the main window and grabs the focus on it.
"""
self.hidden = False
# setting window in all desktops
window_rect = RectCalculator.set_final_window_rect(self.settings, self.window)
self.window.stick()
# add tab must be called before window.show to avoid a
# blank screen before adding the tab.
if not self.get_notebook().has_page():
self.add_tab()
self.window.set_keep_below(False)
if not self.fullscreen_manager.is_fullscreen():
self.window.show_all()
# this is needed because self.window.show_all() results in showing every
# thing which includes the scrollbar too
self.settings.general.triggerOnChangedValue(self.settings.general, "use-scrollbar")
# move the window even when in fullscreen-mode
log.debug("Moving window to: %r", window_rect)
self.window.move(window_rect.x, window_rect.y)
# this works around an issue in fluxbox
if not self.fullscreen_manager.is_fullscreen():
self.settings.general.triggerOnChangedValue(self.settings.general, "window-height")
time = get_server_time(self.window)
# TODO PORT this
# When minized, the window manager seems to refuse to resume
# log.debug("self.window: %s. Dir=%s", type(self.window), dir(self.window))
# is_iconified = self.is_iconified()
# if is_iconified:
# log.debug("Is iconified. Ubuntu Trick => "
# "removing skip_taskbar_hint and skip_pager_hint "
# "so deiconify can work!")
# self.get_widget('window-root').set_skip_taskbar_hint(False)
# self.get_widget('window-root').set_skip_pager_hint(False)
# self.get_widget('window-root').set_urgency_hint(False)
# log.debug("get_skip_taskbar_hint: {}".format(
# self.get_widget('window-root').get_skip_taskbar_hint()))
# log.debug("get_skip_pager_hint: {}".format(
# self.get_widget('window-root').get_skip_pager_hint()))
# log.debug("get_urgency_hint: {}".format(
# self.get_widget('window-root').get_urgency_hint()))
# glib.timeout_add_seconds(1, lambda: self.timeout_restore(time))
#
log.debug("order to present and deiconify")
self.window.present()
self.window.deiconify()
self.window.show()
self.window.get_window().focus(time)
self.window.set_type_hint(Gdk.WindowTypeHint.DOCK)
self.window.set_type_hint(Gdk.WindowTypeHint.NORMAL)
# log.debug("Restoring skip_taskbar_hint and skip_pager_hint")
# if is_iconified:
# self.get_widget('window-root').set_skip_taskbar_hint(False)
# self.get_widget('window-root').set_skip_pager_hint(False)
# self.get_widget('window-root').set_urgency_hint(False)
# This is here because vte color configuration works only after the
# widget is shown.
self.settings.styleFont.triggerOnChangedValue(self.settings.styleFont, "color")
self.settings.styleBackground.triggerOnChangedValue(self.settings.styleBackground, "color")
log.debug("Current window position: %r", self.window.get_position())
self.restore_pending_terminal_split()
self.execute_hook("show")
def hide_from_remote(self):
"""
Hides the main window of the terminal and sets the visible
flag to False.
"""
log.debug("hide from remote")
self.forceHide = True
self.hide()
def show_from_remote(self):
"""Show the main window of the terminal and sets the visible
flag to False.
"""
log.debug("show from remote")
self.forceHide = True
self.show()
def hide(self):
"""Hides the main window of the terminal and sets the visible
flag to False.
"""
if not HidePrevention(self.window).may_hide():
return
self.hidden = True
self.get_widget("window-root").unstick()
self.window.hide() # Don't use hide_all here!
# Hide popover
self.notebook_manager.get_current_notebook().popover.hide()
def force_move_if_shown(self):
if not self.hidden:
# when displayed, GTK might refuse to move the window (X or Y position). Just hide and
# redisplay it so the final position is correct
log.debug("FORCING HIDE")
self.hide()
log.debug("FORCING SHOW")
self.show()
# -- configuration --
def load_config(self, terminal_uuid=None):
""""Just a proxy for all the configuration stuff.
"""
user_data = {}
if terminal_uuid:
user_data["terminal_uuid"] = terminal_uuid
self.settings.general.triggerOnChangedValue(
self.settings.general, "use-trayicon", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "prompt-on-quit", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "prompt-on-close-tab", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "window-tabbar", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "fullscreen-hide-tabbar", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "mouse-display", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "display-n", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "window-ontop", user_data=user_data
)
if not self.fullscreen_manager.is_fullscreen():
self.settings.general.triggerOnChangedValue(
self.settings.general, "window-height", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "window-width", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "use-scrollbar", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "history-size", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "infinite-history", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "use-vte-titles", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "set-window-title", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "display-tab-names", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "max-tab-name-length", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "quick-open-enable", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "quick-open-command-line", user_data=user_data
)
self.settings.style.triggerOnChangedValue(
self.settings.style, "cursor-shape", user_data=user_data
)
self.settings.styleFont.triggerOnChangedValue(
self.settings.styleFont, "style", user_data=user_data
)
self.settings.styleFont.triggerOnChangedValue(
self.settings.styleFont, "palette", user_data=user_data
)
self.settings.styleFont.triggerOnChangedValue(
self.settings.styleFont, "palette-name", user_data=user_data
)
self.settings.styleFont.triggerOnChangedValue(
self.settings.styleFont, "allow-bold", user_data=user_data
)
self.settings.styleBackground.triggerOnChangedValue(
self.settings.styleBackground, "transparency", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "use-default-font", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "compat-backspace", user_data=user_data
)
self.settings.general.triggerOnChangedValue(
self.settings.general, "compat-delete", user_data=user_data
)
def accel_search_terminal(self, *args):
nb = self.get_notebook()
term = nb.get_current_terminal()
box = nb.get_nth_page(nb.find_page_index_by_terminal(term))
# Debounce it
current_time = pytime.time()
if current_time - self.prev_accel_search_terminal_time < 0.3:
return
self.prev_accel_search_terminal_time = current_time
if box.search_revealer.get_reveal_child():
if box.search_entry.has_focus():
box.hide_search_box()
else:
# The box was showed, but out of focus
# Don't hide it, re-grab the focus to search entry
box.search_entry.grab_focus()
else:
box.show_search_box()
def accel_quit(self, *args):
"""Callback to prompt the user whether to quit Guake or not.
"""
procs = self.notebook_manager.get_running_fg_processes_count()
tabs = self.notebook_manager.get_n_pages()
notebooks = self.notebook_manager.get_n_notebooks()
prompt_cfg = self.settings.general.get_boolean("prompt-on-quit")
prompt_tab_cfg = self.settings.general.get_int("prompt-on-close-tab")
# "Prompt on tab close" config overrides "prompt on quit" config
if prompt_cfg or (prompt_tab_cfg == 1 and procs > 0) or (prompt_tab_cfg == 2):
log.debug("Remaining procs=%r", procs)
if PromptQuitDialog(self.window, procs, tabs, notebooks).quit():
log.info("Quitting Guake")
Gtk.main_quit()
else:
log.info("Quitting Guake")
Gtk.main_quit()
def accel_reset_terminal(self, *args):
# TODO KEYBINDINGS ONLY
"""Callback to reset and clean the terminal"""
HidePrevention(self.window).prevent()
current_term = self.get_notebook().get_current_terminal()
current_term.reset(True, True)
HidePrevention(self.window).allow()
return True
def accel_zoom_in(self, *args):
"""Callback to zoom in.
"""
for term in self.get_notebook().iter_terminals():
term.increase_font_size()
return True
def accel_zoom_out(self, *args):
"""Callback to zoom out.
"""
for term in self.get_notebook().iter_terminals():
term.decrease_font_size()
return True
def accel_increase_height(self, *args):
"""Callback to increase height.
"""
height = self.settings.general.get_int("window-height")
self.settings.general.set_int("window-height", min(height + 2, 100))
return True
def accel_decrease_height(self, *args):
"""Callback to decrease height.
"""
height = self.settings.general.get_int("window-height")
self.settings.general.set_int("window-height", max(height - 2, 0))
return True
def accel_increase_transparency(self, *args):
"""Callback to increase transparency.
"""
transparency = self.settings.styleBackground.get_int("transparency")
if int(transparency) > 0:
self.settings.styleBackground.set_int("transparency", int(transparency) - 2)
return True
def accel_decrease_transparency(self, *args):
"""Callback to decrease transparency.
"""
transparency = self.settings.styleBackground.get_int("transparency")
if int(transparency) < MAX_TRANSPARENCY:
self.settings.styleBackground.set_int("transparency", int(transparency) + 2)
return True
def accel_toggle_transparency(self, *args):
"""Callback to toggle transparency.
"""
self.transparency_toggled = not self.transparency_toggled
self.settings.styleBackground.triggerOnChangedValue(
self.settings.styleBackground, "transparency"
)
return True
def accel_add(self, *args):
"""Callback to add a new tab. Called by the accel key.
"""
self.add_tab()
return True
def accel_add_home(self, *args):
"""Callback to add a new tab in home directory. Called by the accel key.
"""
self.add_tab(os.environ["HOME"])
return True
def accel_prev(self, *args):
"""Callback to go to the previous tab. Called by the accel key.
"""
if self.get_notebook().get_current_page() == 0:
self.get_notebook().set_current_page(self.get_notebook().get_n_pages() - 1)
else:
self.get_notebook().prev_page()
return True
def accel_next(self, *args):
"""Callback to go to the next tab. Called by the accel key.
"""
if self.get_notebook().get_current_page() + 1 == self.get_notebook().get_n_pages():
self.get_notebook().set_current_page(0)
else:
self.get_notebook().next_page()
return True
def accel_move_tab_left(self, *args):
# TODO KEYBINDINGS ONLY
""" Callback to move a tab to the left """
pos = self.get_notebook().get_current_page()
if pos != 0:
self.move_tab(pos, pos - 1)
return True
def accel_move_tab_right(self, *args):
# TODO KEYBINDINGS ONLY
""" Callback to move a tab to the right """
pos = self.get_notebook().get_current_page()
if pos != self.get_notebook().get_n_pages() - 1:
self.move_tab(pos, pos + 1)
return True
def move_tab(self, old_tab_pos, new_tab_pos):
self.get_notebook().reorder_child(
self.get_notebook().get_nth_page(old_tab_pos), new_tab_pos
)
self.get_notebook().set_current_page(new_tab_pos)
def gen_accel_switch_tabN(self, N):
"""Generates callback (which called by accel key) to go to the Nth tab.
"""
def callback(*args):
if 0 <= N < self.get_notebook().get_n_pages():
self.get_notebook().set_current_page(N)
return True
return callback
def accel_switch_tab_last(self, *args):
last_tab = self.get_notebook().get_n_pages() - 1
self.get_notebook().set_current_page(last_tab)
return True
def accel_rename_current_tab(self, *args):
"""Callback to show the rename tab dialog. Called by the accel
key.
"""
page_num = self.get_notebook().get_current_page()
page = self.get_notebook().get_nth_page(page_num)
self.get_notebook().get_tab_label(page).on_rename(None)
return True
def accel_copy_clipboard(self, *args):
# TODO KEYBINDINGS ONLY
"""Callback to copy text in the shown terminal. Called by the
accel key.
"""
self.get_notebook().get_current_terminal().copy_clipboard()
return True
def accel_paste_clipboard(self, *args):
# TODO KEYBINDINGS ONLY
"""Callback to paste text in the shown terminal. Called by the
accel key.
"""
self.get_notebook().get_current_terminal().paste_clipboard()
return True
def accel_toggle_hide_on_lose_focus(self, *args):
"""Callback toggle whether the window should hide when it loses
focus. Called by the accel key.
"""
if self.settings.general.get_boolean("window-losefocus"):
self.settings.general.set_boolean("window-losefocus", False)
else:
self.settings.general.set_boolean("window-losefocus", True)
return True
def accel_toggle_fullscreen(self, *args):
self.fullscreen_manager.toggle()
return True
def fullscreen(self):
self.fullscreen_manager.fullscreen()
def unfullscreen(self):
self.fullscreen_manager.unfullscreen()
# -- callbacks --
def recompute_tabs_titles(self):
"""Updates labels on all tabs. This is required when `self.display_tab_names`
changes
"""
use_vte_titles = self.settings.general.get_boolean("use-vte-titles")
if not use_vte_titles:
return
# TODO NOTEBOOK this code only works if there is only one terminal in a
# page, this need to be rewritten
for terminal in self.get_notebook().iter_terminals():
page_num = self.get_notebook().page_num(terminal.get_parent())
self.get_notebook().rename_page(page_num, self.compute_tab_title(terminal), False)
def compute_tab_title(self, vte):
"""Compute the tab title
"""
vte_title = vte.get_window_title() or _("Terminal")
try:
current_directory = vte.get_current_directory()
if self.display_tab_names == 1 and vte_title.endswith(current_directory):
parts = current_directory.split("/")
parts = [s[:1] for s in parts[:-1]] + [parts[-1]]
vte_title = vte_title[: len(vte_title) - len(current_directory)] + "/".join(parts)
if self.display_tab_names == 2:
vte_title = current_directory.split("/")[-1]
if not vte_title:
vte_title = "(root)"
except OSError:
pass
return TabNameUtils.shorten(vte_title, self.settings)
@save_tabs_when_changed
def on_terminal_title_changed(self, vte, term):
# box must be a page
if not term.get_parent():
return
box = term.get_parent().get_root_box()
use_vte_titles = self.settings.general.get_boolean("use-vte-titles")
if not use_vte_titles:
return
# NOTE: Try our best to find the page_num inside all notebooks
# this may return -1, should be checked ;)
nb = self.get_notebook()
page_num = nb.page_num(box)
for nb in self.notebook_manager.iter_notebooks():
page_num = nb.page_num(box)
if page_num != -1:
break
# if tab has been renamed by user, don't override.
if not getattr(box, "custom_label_set", False):
title = self.compute_tab_title(vte)
nb.rename_page(page_num, title, False)
self.update_window_title(title)
else:
text = nb.get_tab_text_page(box)
if text:
self.update_window_title(text)
def update_window_title(self, title):
if self.settings.general.get_boolean("set-window-title") is True:
self.window.set_title(title)
else:
self.window.set_title(self.default_window_title)
# TODO PORT reimplement drag and drop text on terminal
# -- tab related functions --
def close_tab(self, *args):
"""Closes the current tab.
"""
prompt_cfg = self.settings.general.get_int("prompt-on-close-tab")
self.get_notebook().delete_page_current(prompt=prompt_cfg)
def rename_tab_uuid(self, term_uuid, new_text, user_set=True):
"""Rename an already added tab by its UUID
"""
term_uuid = uuid.UUID(term_uuid)
(page_index,) = (
index
for index, t in enumerate(self.get_notebook().iter_terminals())
if t.get_uuid() == term_uuid
)
self.get_notebook().rename_page(page_index, new_text, user_set)
def rename_current_tab(self, new_text, user_set=False):
page_num = self.get_notebook().get_current_page()
self.get_notebook().rename_page(page_num, new_text, user_set)
def terminal_spawned(self, notebook, terminal, pid):
self.load_config(terminal_uuid=terminal.uuid)
terminal.handler_ids.append(
terminal.connect("window-title-changed", self.on_terminal_title_changed, terminal)
)
@save_tabs_when_changed
def add_tab(self, directory=None):
"""Adds a new tab to the terminal notebook.
"""
position = None
if self.settings.general.get_boolean("new-tab-after"):
position = 1 + self.get_notebook().get_current_page()
self.get_notebook().new_page_with_focus(directory, position=position)
def find_tab(self, directory=None):
log.debug("find")
# TODO SEARCH
HidePrevention(self.window).prevent()
search_text = Gtk.TextView()
dialog = Gtk.Dialog(
_("Find"),
self.window,
Gtk.DialogFlags.DESTROY_WITH_PARENT,
(
_("Forward"),
RESPONSE_FORWARD,
_("Backward"),
RESPONSE_BACKWARD,
Gtk.STOCK_CANCEL,
Gtk.ResponseType.NONE,
),
)
dialog.vbox.pack_end(search_text, True, True, 0)
dialog.buffer = search_text.get_buffer()
dialog.connect("response", self._dialog_response_callback)
search_text.show()
search_text.grab_focus()
dialog.show_all()
# Note: beware to reset preventHide when closing the find dialog
def _dialog_response_callback(self, dialog, response_id):
if response_id not in (RESPONSE_FORWARD, RESPONSE_BACKWARD):
dialog.destroy()
HidePrevention(self.window).allow()
return
start, end = dialog.buffer.get_bounds()
search_string = start.get_text(end)
log.debug(
"Searching for %r %s\n",
search_string,
"forward" if response_id == RESPONSE_FORWARD else "backward",
)
current_term = self.get_notebook().get_current_terminal()
log.debug("type: %r", type(current_term))
log.debug("dir: %r", dir(current_term))
current_term.search_set_gregex()
current_term.search_get_gregex()
# buffer = self.text_view.get_buffer()
# if response_id == RESPONSE_FORWARD:
# buffer.search_forward(search_string, self)
# elif response_id == RESPONSE_BACKWARD:
# buffer.search_backward(search_string, self)
def page_deleted(self, *args):
if not self.get_notebook().has_page():
self.hide()
# avoiding the delay on next Guake show request
self.add_tab()
else:
self.set_terminal_focus()
self.was_deleted_tab = True
self.display_tab_names = self.settings.general.get_int("display-tab-names")
self.recompute_tabs_titles()
def set_terminal_focus(self):
"""Grabs the focus on the current tab.
"""
self.get_notebook().set_current_page(self.get_notebook().get_current_page())
def get_selected_uuidtab(self):
# TODO DBUS ONLY
"""Returns the uuid of the current selected terminal
"""
page_num = self.get_notebook().get_current_page()
terminals = self.get_notebook().get_terminals_for_page(page_num)
return str(terminals[0].get_uuid())
def search_on_web(self, *args):
"""Search for the selected text on the web
"""
# TODO KEYBINDINGS ONLY
current_term = self.get_notebook().get_current_terminal()
if current_term.get_has_selection():
current_term.copy_clipboard()
guake_clipboard = Gtk.Clipboard.get_default(self.window.get_display())
search_query = guake_clipboard.wait_for_text()
search_query = quote_plus(search_query)
if search_query:
# TODO search provider should be selectable (someone might
# prefer bing.com, the internet is a strange place ¯\_(ツ)_/¯ )
search_url = "https://www.google.com/#q={!s}&safe=off".format(search_query,)
Gtk.show_uri(self.window.get_screen(), search_url, get_server_time(self.window))
return True
def set_tab_position(self, *args):
if self.settings.general.get_boolean("tab-ontop"):
self.get_notebook().set_tab_pos(Gtk.PositionType.TOP)
else:
self.get_notebook().set_tab_pos(Gtk.PositionType.BOTTOM)
def execute_hook(self, event_name):
"""Execute shell commands related to current event_name"""
hook = self.settings.hooks.get_string("{!s}".format(event_name))
if hook is not None and hook != "":
hook = hook.split()
try:
subprocess.Popen(hook)
except OSError as oserr:
if oserr.errno == 8:
log.error(
"Hook execution failed! Check shebang at first line of %s!", hook,
)
log.debug(traceback.format_exc())
else:
log.error(str(oserr))
except Exception as e:
log.error("hook execution failed! %s", e)
log.debug(traceback.format_exc())
else:
log.debug("hook on event %s has been executed", event_name)
@save_tabs_when_changed
def on_page_reorder(self, notebook, child, page_num):
# Yep, just used for save tabs when changed
pass
def get_xdg_config_directory(self):
xdg_config_home = os.environ.get("XDG_CONFIG_HOME", "~/.config")
return Path(xdg_config_home, "guake").expanduser()
def save_tabs(self, filename="session.json"):
config = {
"schema_version": TABS_SESSION_SCHEMA_VERSION,
"timestamp": int(pytime.time()),
"workspace": {},
}
for key, nb in self.notebook_manager.get_notebooks().items():
tabs = []
for index in range(nb.get_n_pages()):
try:
page = nb.get_nth_page(index)
if page.child:
panes = []
page.save_box_layout(page.child, panes)
tabs.append(
{
"panes": panes,
"label": nb.get_tab_text_index(index),
"custom_label_set": getattr(page, "custom_label_set", False),
}
)
except FileNotFoundError:
# discard same broken tabs
pass
# NOTE: Maybe we will have frame inside the workspace in future
# So lets use list to store the tabs (as for each frame)
config["workspace"][key] = [tabs]
if not self.get_xdg_config_directory().exists():
self.get_xdg_config_directory().mkdir(parents=True)
session_file = self.get_xdg_config_directory() / filename
with session_file.open("w") as f:
json.dump(config, f, ensure_ascii=False, indent=4)
log.info("Guake tabs saved to %s", session_file)
def restore_tabs(self, filename="session.json", suppress_notify=False):
session_file = self.get_xdg_config_directory() / filename
if not session_file.exists():
log.info("Cannot find session.json file")
return
with session_file.open() as f:
try:
config = json.load(f)
except Exception:
log.warning("%s is broken", session_file)
shutil.copy(
session_file, self.get_xdg_config_directory() / "{0}.bak".format(filename),
)
img_filename = pixmapfile("guake-notification.png")
notifier.showMessage(
_("Guake Terminal"),
_(
"Your {session_filename} file is broken, backup to {session_filename}.bak"
).format(session_filename=filename),
img_filename,
)
return
# Check schema_version exist
if "schema_version" not in config:
img_filename = pixmapfile("guake-notification.png")
notifier.showMessage(
_("Guake Terminal"),
_(
"Tabs session restore abort.\n"
"Your session file ({session_filename}) missing schema_version as key"
).format(session_filename=session_file),
img_filename,
)
return
# Check schema version is not higher than current version
if config["schema_version"] > TABS_SESSION_SCHEMA_VERSION:
img_filename = pixmapfile("guake-notification.png")
notifier.showMessage(
_("Guake Terminal"),
_(
"Tabs session restore abort.\n"
"Your session file schema version is higher than current version "
"({session_file_schema_version} > {current_schema_version})."
).format(
session_file_schema_version=config["schema_version"],
current_schema_version=TABS_SESSION_SCHEMA_VERSION,
),
img_filename,
)
return
# Disable auto save tabs
v = self.settings.general.get_boolean("save-tabs-when-changed")
self.settings.general.set_boolean("save-tabs-when-changed", False)
# Restore all tabs for all workspaces
self.pending_restore_page_split = []
self._failed_restore_page_split = []
try:
for key, frames in config["workspace"].items():
nb = self.notebook_manager.get_notebook(int(key))
current_pages = nb.get_n_pages()
# Restore each frames' tabs from config
# NOTE: If frame implement in future, we will need to update this code
for tabs in frames:
for index, tab in enumerate(tabs):
directory = (
tab["panes"][0]["directory"]
if len(tab.get("panes", [])) == 1
else tab.get("directory", None)
)
box, page_num, term = nb.new_page_with_focus(
directory, tab["label"], tab["custom_label_set"]
)
if tab.get("panes", False):
# if directory:
# continue
box.restore_box_layout(box.child, tab["panes"])
# Remove original pages in notebook
for i in range(current_pages):
nb.delete_page(0)
except KeyError:
log.warning("%s schema is broken", session_file)
shutil.copy(
session_file, self.get_xdg_config_directory() / "{}.bak".format(filename),
)
with (self.get_xdg_config_directory() / "{}.log.err".format(filename)).open("w") as f:
traceback.print_exc(file=f)
img_filename = pixmapfile("guake-notification.png")
notifier.showMessage(
_("Guake Terminal"),
_(
"Your {session_filename} schema is broken, backup to {session_filename}.bak, "
"and error message has been saved to {session_filename}.log.err.".format(
session_filename=filename
)
),
img_filename,
)
# Reset auto save tabs
self.settings.general.set_boolean("save-tabs-when-changed", v)
# Notify the user
if self.settings.general.get_boolean("restore-tabs-notify") and not suppress_notify:
filename = pixmapfile("guake-notification.png")
notifier.showMessage(_("Guake Terminal"), _("Your tabs has been restored!"), filename)
log.info("Guake tabs restored from %s", session_file)