-
-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathnautilus_terminal.py
652 lines (536 loc) · 22.4 KB
/
nautilus_terminal.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
import os
import signal
import sys
from . import logger
from . import helpers
from . import color_helpers
from . import nautilus_accels_helpers
from .about_dialog import AboutDialog
import gi
gi.require_version("Vte", "2.91")
from gi.repository import GLib, Gio, Gtk, Gdk, Vte, Pango # noqa: E402
_EXPAND_WIDGETS = [
"GtkOverlay",
"NautilusCanvasView",
"NautilusViewIconController",
"NautilusListView",
]
_AUTO_CLEAN_SOFT = 1
_AUTO_CLEAN_HARD = 2
def _vte_terminal_feed_child(vte_terminal, text):
if sys.version_info.major >= 3:
text = text.encode("utf-8")
try:
# Old call
return vte_terminal.feed_child(text, len(text) + 1)
except TypeError:
# Newer call
return vte_terminal.feed_child(text)
def _find_nautilus_terminal_vpanel(crowbar):
widget = crowbar
while widget:
if widget.get_name() == "GtkVPaned" and hasattr(
widget, "_nt_instance"
):
return widget
widget = widget.get_parent()
return None
def _find_parent_widget(widget, parent_widget_name):
while widget:
if widget.get_name() == parent_widget_name:
return widget
widget = widget.get_parent()
return None
def create_or_update_natilus_terminal(crowbar):
vpanel = _find_nautilus_terminal_vpanel(crowbar)
# Nautilus Terminal already inserted in this tab, update its path
if vpanel:
logger.log("NautilusTerminal instance found: updating its path...")
vpanel._nt_instance.change_directory(crowbar.path)
# may update view
vpanel._nt_instance.update_ui()
return vpanel._nt_instance
# New tab, a new Nautilus Terminal instance must be created
logger.log(
"No NautilusTerminal instance found (new tab): creating a new NautilusTerminal..."
)
nautilus_window_slot = _find_parent_widget(crowbar, "NautilusWindowSlot")
if not nautilus_window_slot:
logger.warn(
"Unable to locate the NautilusWindowSlot widget: Nautilus Terminal will not be injected!"
)
return
return NautilusTerminal(
nautilus_window_slot,
crowbar.nautilus_window,
crowbar.nautilus_app,
crowbar.path,
)
class NautilusTerminal(object):
def __init__(self, parent_widget, nautilus_window, nautilus_app, cwd):
self._parent_widget = parent_widget # NautilusWindowSlot
self._nautilus_window = nautilus_window
self._nautilus_app = nautilus_app
self._vbox = None
self._cwd = cwd
self._settings = helpers.get_application_settings()
# Allows settings to be defined in dconf-editor even if
# the schema is not installed...
helpers.set_all_settings(self._settings)
self._ui_vpanel = None
self._ui_terminal = None
self._terminal_requested_visibility = self._settings.get_boolean(
"default-show-terminal"
)
self._terminal_focus_on_init = self._settings.get_boolean(
"default-focus-terminal"
)
self._terminal_bottom = self._settings.get_boolean("terminal-bottom")
self._auto_clean = self._settings.get_enum("auto-clean")
self._auto_cut_user_input = self._settings.get_boolean(
"auto-cut-user-input"
)
self._nterm_action_group = None
self._ntermwin_action_group = None
self._shell_pid = 0
self._shell_killed = False
nautilus_accels_helpers.backup_nautilus_accels(
nautilus_app, nautilus_window
)
self._build_and_inject_ui()
self._build_actions()
self._insert_ntermwin_action_group_in_current_window()
self._build_accels()
# Set if the terminal should be visible by default.
# Will spawn the shell automatically if the terminal is visible
self.set_terminal_visible(
self._terminal_requested_visibility, self._terminal_focus_on_init
)
def change_directory(self, path):
# "virtual" location (trash:///, network:///,...)
# No cd & hide the Terminal
if not path:
logger.log(
'NautilusTerminal.change_directory: terminal hidden: navigating to a "virtual" location'
)
self._ui_terminal.set_visible(False)
return
self._cwd = path
# Makes the terminal visible again if it was hidden by navigating to a
# "virtual" location
if (
self.get_terminal_visible()
!= self.get_terminal_requested_visibility()
):
self.set_terminal_visible(focus=False)
# Do not navigate if the terminal is not visible
if not self.get_terminal_visible():
logger.log(
"NautilusTerminal.change_directory: current directory NOT changed to %s (terminal not visible)"
% path
)
return
# Do not "cd" if the shell's cwd is already the same as the targeted path
if helpers.get_process_cwd(self._shell_pid) == path:
return
logger.log(
"NautilusTerminal.change_directory: current directory changed to %s"
% path
)
command = " cd %s " % helpers.escape_path_for_shell(self._cwd)
if self.get_auto_clean() == _AUTO_CLEAN_HARD:
command += "&& clear "
self._inject_command(command)
if self.get_auto_clean() == _AUTO_CLEAN_SOFT:
self._emit_key_press("l", Gdk.ModifierType.CONTROL_MASK)
def stash_current_termianl_content(self):
# move to the end of line in terminal
self._emit_key_press("e", Gdk.ModifierType.CONTROL_MASK)
# cut all content in line to the left
self._emit_key_press("u", Gdk.ModifierType.CONTROL_MASK)
def get_auto_cut_user_input(self):
return self._auto_cut_user_input
def get_auto_clean(self):
return self._auto_clean
def get_terminal_requested_visibility(self):
"""Does the user requested the terminal to be visible?
This state can be different to the terminal widget real state, for
example the terminal can be requested visible by the user but hidden
because navigating to the trash)?
"""
return self._terminal_requested_visibility
def get_terminal_visible(self):
"""Does the terminal widget is visible?"""
return self._ui_terminal.get_visible()
def set_terminal_visible(self, visible=None, focus=True):
if visible is None:
visible = self._terminal_requested_visibility
self._ui_terminal.set_visible(visible)
self._terminal_requested_visibility = visible
# Spawn a shell if it is not yet spawned (if the terminal has never
# been visible before)
if visible and not self._shell_pid:
self._spawn_shell()
# Update the directory as the terminal does not "cd" when it is hidden
if visible:
self.change_directory(self._cwd)
# Focus the terminal
if visible and focus:
self._ui_terminal.grab_focus()
def shell_is_busy(self):
return helpers.process_has_child(self._shell_pid)
def _inject_command(self, command):
# Do not inject the command if the shell has something running in
if self.shell_is_busy():
logger.warn(
"NautilusTerminal._inject_command: command '%s' not injected (shell busy)"
% command
)
return
logger.log("NautilusTerminal._inject_command: %s" % command)
# Remove any user inputs before injecting the command
if self.get_auto_cut_user_input():
self.stash_current_termianl_content()
_vte_terminal_feed_child(self._ui_terminal, "%s\n" % command)
def _emit_key_press(self, key, state=0):
event = Gdk.Event().new(Gdk.EventType.KEY_PRESS)
event.state = state
event.keyval = Gdk.keyval_from_name(key)
event.window = self._ui_terminal.get_window()
event.send_event = True
self._ui_terminal.emit("key-press-event", event)
def update_ui(self):
for widget in self._parent_widget:
if widget.get_name() in _EXPAND_WIDGETS:
self._parent_widget.remove(widget)
self._vbox.pack_start(widget, True, True, 0)
def _build_and_inject_ui(self):
# GtkPaned (vpanel) injection:
#
# To inject our terminal in Nautilus, we first have to empty the
# NautilusWindowSlot widget (parent widget) of the current view to make
# some free space to insert a GtkPaned widget. Then we will insert back
# the Nautilus' widgets we removed in the bottom part of the GtkPaned.
self._ui_vpanel = Gtk.VPaned(visible=True)
self._ui_vpanel._nt_instance = self
self._vbox = Gtk.VBox(visible=True)
if self._terminal_bottom:
self._ui_vpanel.pack1(self._vbox, resize=True, shrink=True)
else:
self._ui_vpanel.pack2(self._vbox, resize=True, shrink=True)
for widget in self._parent_widget:
self._parent_widget.remove(widget)
expand = widget.get_name() in _EXPAND_WIDGETS
self._vbox.pack_start(widget, expand, expand, 0)
self._parent_widget.pack_start(self._ui_vpanel, True, True, 0)
# Terminal creation:
#
# We can now create our VteTerminal and insert it in the top part of
# our GtkPaned.
self._ui_terminal = Vte.Terminal()
settings_font = self._settings.get_string("custom-font")
self._ui_terminal.set_font(Pango.FontDescription(settings_font))
self._ui_terminal.set_audible_bell(False)
self._ui_terminal.connect(
"child-exited", self._on_terminal_child_existed
)
if self._terminal_bottom:
self._ui_vpanel.pack2(
self._ui_terminal, resize=False, shrink=False
)
else:
self._ui_vpanel.pack1(
self._ui_terminal, resize=False, shrink=False
)
TERMINAL_CHAR_HEIGHT = self._ui_terminal.get_char_height()
TERMINAL_BORDER_WIDTH = 1
TERMINAL_MIN_HEIGHT = self._settings.get_uint("min-terminal-height")
self._ui_terminal.set_property(
"height-request",
TERMINAL_CHAR_HEIGHT * TERMINAL_MIN_HEIGHT
+ TERMINAL_BORDER_WIDTH * 2,
)
# Terminal foreground and background colors
fg_color = (255, 255, 255)
bg_color = (0, 0, 0)
settings_fg_color = self._settings.get_string("foreground-color")
settings_bg_color = self._settings.get_string("background-color")
if color_helpers.is_color(settings_fg_color):
fg_color = color_helpers.parse_color_string(settings_fg_color)
if color_helpers.is_color(settings_bg_color):
bg_color = color_helpers.parse_color_string(settings_bg_color)
foreground = Gdk.RGBA(
fg_color[0] / 255.0,
fg_color[1] / 255.0,
fg_color[2] / 255.0,
1,
)
background = Gdk.RGBA(
bg_color[0] / 255.0,
bg_color[1] / 255.0,
bg_color[2] / 255.0,
1,
)
# Terminal color palette
settings_color_palette = self._settings.get_strv("color-palette")
palette_colors = []
if any(settings_color_palette) and len(settings_color_palette) in (
8,
16,
232,
256,
):
for color_code in settings_color_palette:
if not color_helpers.is_color(color_code):
color_code = "White" # Invalid colors default to white
color = color_helpers.parse_color_string(color_code)
color_rgba = Gdk.RGBA(
color[0] / 255.0,
color[1] / 255.0,
color[2] / 255.0,
1,
)
palette_colors.append(color_rgba)
self._ui_terminal.set_colors(foreground, background, palette_colors)
# Highlight colors
foreground_hl_color = None
background_hl_color = None
settings_fg_hl_color = self._settings.get_string(
"foreground-highlight-color"
)
settings_bg_hl_color = self._settings.get_string(
"background-highlight-color"
)
if color_helpers.is_color(settings_fg_hl_color):
fg_hl_color = color_helpers.parse_color_string(
settings_fg_hl_color
)
foreground_hl_color = Gdk.RGBA(
fg_hl_color[0] / 255.0,
fg_hl_color[1] / 255.0,
fg_hl_color[2] / 255.0,
1,
)
if color_helpers.is_color(settings_bg_hl_color):
bg_hl_color = color_helpers.parse_color_string(
settings_bg_hl_color
)
background_hl_color = Gdk.RGBA(
bg_hl_color[0] / 255.0,
bg_hl_color[1] / 255.0,
bg_hl_color[2] / 255.0,
1,
)
self._ui_terminal.set_color_highlight_foreground(foreground_hl_color)
self._ui_terminal.set_color_highlight(background_hl_color)
# Bold text as bright
self._ui_terminal.set_bold_is_bright(
self._settings.get_boolean("bold-is-bright")
)
# File drag & drop support
self._ui_terminal.drag_dest_set(
Gtk.DestDefaults.MOTION
| Gtk.DestDefaults.HIGHLIGHT
| Gtk.DestDefaults.DROP,
[Gtk.TargetEntry.new("text/uri-list", 0, 0)],
Gdk.DragAction.COPY,
)
self._ui_terminal.drag_dest_add_uri_targets()
self._ui_terminal.connect(
"drag-data-received", self._on_terminal_drag_data_received
)
# Disabling Nautilus' accels when the terminal is focused:
#
# When the terminal is focused, we MUST remove ALL Nautilus accels to
# be able to use properly the terminal (else we cannot type some
# characters like "/", "~",... because the actions of the corresponding
# accels are called). Of course we have also to restore the accels when
# the terminal is no more focused.
self._ui_terminal.connect(
"focus-in-event", self._on_terminal_focus_in_event
)
self._ui_terminal.connect(
"focus-out-event", self._on_terminal_focus_out_event
)
# Register the window-level action group of the currently displayed
# terminal
#
# When the active tab changes, we have to register on the nautilus
# window the corresponding action group to allow related accels to work
# properly. The "map" event of the NautilusWindowSlot (our parent
# widget) is called when its tab become active.
self._parent_widget.connect(
"map", self._on_nautilus_window_slot_mapped
)
# Kill the shell when the tab is closed:
#
# Finally, we have to listen to some events of our parent widget (the
# NautilusWindowSlot) to guess when the tab is closed in order to
# kill the shell (else we will have plenty zombie shell running in
# background).
#
# * When the tab (or the window) is closed, the "unrealize" event is
# called so we have to kill the shell when the parent widget emit
# this event...
#
# * BUT when the tab is drag & dropped outside of the window (to open
# it in a new window), this event is also emitted... and the
# "realize" event is emitted right after. So we have to spawn a new
# shell if that happen...
self._parent_widget.connect(
"unrealize", self._on_nautilus_window_slot_unrealized
)
self._parent_widget.connect(
"realize", self._on_nautilus_window_slot_realized
)
# Terminal Context Menu
self._ui_menu = Gtk.Menu()
menu_item = Gtk.ImageMenuItem.new_from_stock("gtk-copy", None)
menu_item.connect_after(
"activate", lambda w: self._ui_terminal.copy_clipboard()
)
self._ui_menu.add(menu_item)
menu_item = Gtk.ImageMenuItem.new_from_stock("gtk-paste", None)
menu_item.connect_after(
"activate", lambda w: self._ui_terminal.paste_clipboard()
)
self._ui_menu.add(menu_item)
self._ui_menu.add(Gtk.SeparatorMenuItem())
menu_item = Gtk.ImageMenuItem.new_from_stock("gtk-preferences", None)
menu_item.connect_after("activate", self._on_menu_preferences_activate)
self._ui_menu.add(menu_item)
menu_item = Gtk.ImageMenuItem.new_from_stock("gtk-about", None)
menu_item.connect_after("activate", self._on_menu_about_activate)
self._ui_menu.add(menu_item)
self._ui_menu.show_all()
self._ui_terminal.connect(
"button-release-event", self._on_terminal_popup_menu
)
def _build_actions(self):
# nterm action group
self._nterm_action_group = Gio.SimpleActionGroup()
self._ui_terminal.insert_action_group(
"nterm", self._nterm_action_group
)
copy_action = Gio.SimpleAction(name="copy")
copy_action.connect("activate", self._on_nterm_copy_action_activated)
self._nterm_action_group.add_action(copy_action)
paste_action = Gio.SimpleAction(name="paste")
paste_action.connect("activate", self._on_nterm_paste_action_activated)
self._nterm_action_group.add_action(paste_action)
# ntermwin action group
self._ntermwin_action_group = Gio.SimpleActionGroup()
terminal_visible_action = Gio.SimpleAction(name="terminal-visible")
terminal_visible_action.connect(
"activate", self._on_ntermwin_terminal_visible_action_activated
)
self._ntermwin_action_group.add_action(terminal_visible_action)
def _insert_ntermwin_action_group_in_current_window(self):
self._nautilus_window.insert_action_group(
"ntermwin", self._ntermwin_action_group
)
def _build_accels(self):
# nterm
self._nautilus_app.set_accels_for_action(
"nterm.copy", ["<Primary><Shift>c"]
)
self._nautilus_app.set_accels_for_action(
"nterm.paste", ["<Primary><Shift>v"]
)
# ntermwin
accel = self._settings.get_string("toggle-shortcut")
self._nautilus_app.set_accels_for_action(
"ntermwin.terminal-visible", [accel]
)
def _spawn_shell(self):
if self._shell_pid:
logger.warn(
"NautilusTerminal._spawn_shell: Cannot spawn a new shell: there is already a shell running."
)
return
shell = helpers.get_user_default_shell()
if self._settings.get_boolean("use-custom-command"):
shell = self._settings.get_string("custom-command")
_, self._shell_pid = self._ui_terminal.spawn_sync(
Vte.PtyFlags.DEFAULT,
self._cwd,
[shell],
["INSIDE_NAUTILUS_PYTHON=1"],
GLib.SpawnFlags.SEARCH_PATH,
None,
None,
)
self._shell_killed = False
logger.log(
"NautilusTerminal._spawn_shell: Shell spawned (%s), PID: %i."
% (shell, self._shell_pid)
)
def _kill_shell(self):
if not self._shell_pid:
logger.warn(
"NautilusTerminal._kill_shell: Cannot kill the shell: there is no shell to kill..."
)
return
self._shell_killed = True
try:
os.kill(self._shell_pid, signal.SIGTERM)
os.kill(self._shell_pid, signal.SIGKILL)
except OSError:
logger.error(
"NautilusTerminal._kill_shell: An error occured when killing the shell %i"
% self._shell_pid
)
self._shell_pid = 0
logger.log("Shell %i killed." % self._shell_pid)
self._shell_pid = 0
def _on_nautilus_window_slot_mapped(self, widget):
logger.log("The active tab has changed.")
self._insert_ntermwin_action_group_in_current_window()
def _on_nautilus_window_slot_unrealized(self, widget):
logger.log(
"The tab have (probably) been closed: killing the shell %i"
% self._shell_pid
)
self._kill_shell()
self._nautilus_window = None
def _on_nautilus_window_slot_realized(self, widget):
logger.log("Oops, the tab have NOT been closed: spawning a new shell")
self._spawn_shell()
self._nautilus_window = _find_parent_widget(widget, "NautilusWindow")
self._insert_ntermwin_action_group_in_current_window()
def _on_terminal_focus_in_event(self, widget, event):
nautilus_accels_helpers.remove_nautilus_accels(self._nautilus_app)
def _on_terminal_focus_out_event(self, widget, event):
nautilus_accels_helpers.restore_nautilus_accels(self._nautilus_app)
def _on_terminal_child_existed(self, widget, arg1):
self._shell_pid = 0
if not self._shell_killed:
self._spawn_shell()
def _on_terminal_drag_data_received(
self, widget, context, x, y, data, info, time
):
for uri in data.get_uris():
path = helpers.escape_path_for_shell(helpers.gvfs_uri_to_path(uri))
_vte_terminal_feed_child(self._ui_terminal, "%s " % path)
self._ui_terminal.grab_focus()
def _on_terminal_popup_menu(self, widget, event):
if event.type == Gdk.EventType.BUTTON_RELEASE and event.button != 3:
return
self._ui_menu.popup(None, None, None, None, 3, 0)
def _on_menu_preferences_activate(self, widget):
self._inject_command("dconf-editor /org/flozz/nautilus-terminal")
def _on_menu_about_activate(self, widget):
about_dialog = AboutDialog(parent=self._nautilus_window)
about_dialog.run()
about_dialog.destroy()
def _on_nterm_copy_action_activated(self, action, parameter):
logger.log("nterm.copy action activated")
self._ui_terminal.copy_clipboard()
def _on_nterm_paste_action_activated(self, action, parameter):
logger.log("nterm.paste action activated")
self._ui_terminal.paste_clipboard()
def _on_ntermwin_terminal_visible_action_activated(
self, action, parameter
):
logger.log("ntermwin.terminal-visible action activated")
self.set_terminal_visible(not self.get_terminal_visible())