Skip to content

Commit

Permalink
Add rudimentary terminal to editor's bottom panel
Browse files Browse the repository at this point in the history
In order to use the gdnative library as an editor plugin it was
neccessary to set the `reloadable` property of the gdnlib file to false,
in order to prevent crashes when the godot editor window lost focus.

This may have consequences when recompiling the library.
See: https://docs.godotengine.org/en/3.3/classes/class_gdnativelibrary.html#class-gdnativelibrary-property-reloadable

Still crashes quite frequently and doesn't close child processes
properly.

Part of #43.
  • Loading branch information
lihop committed Jul 11, 2021
1 parent 33e4640 commit 5e25ebb
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 1 deletion.
78 changes: 78 additions & 0 deletions addons/godot_xterm/editor_plugins/terminal/editor_terminal.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
tool
extends "res://addons/godot_xterm/nodes/terminal/terminal.gd"

var editor_settings: EditorSettings
var timer := Timer.new()

onready var pty = $PTY


# Sets terminal colors according to a dictionary that maps terminal color names
# to TextEditor theme color names.
func _set_terminal_colors(color_map: Dictionary) -> void:
for key in color_map.keys():
var val: String = color_map[key]
var color: Color = editor_settings.get_setting("text_editor/highlighting/%s" % val)
theme.set_color(key, "Terminal", color)


func _ready():
if not editor_settings:
return

theme = Theme.new()

# Use the same font as EditorLog.
theme.default_font = get_font("output_source", "EditorFonts")

# Get colors from TextEdit theme. Created using the default (Adaptive) theme
# for reference, but will probably cause strange results if using another theme
# better to use a dedicated terminal theme, rather than relying on this.
_set_terminal_colors(
{
"Black": "caret_background_color",
"Red": "keyword_color",
"Green": "gdscript/node_path_color",
"Yellow": "string_color",
"Blue": "function_color",
"Magenta": "symbol_color",
"Cyan": "gdscript/function_definition_color",
"Dark Grey": "comment_color",
"Light Grey": "text_color",
"Light Red": "breakpoint_color",
"Light Green": "base_type_color",
"Light Yellow": "search_result_color",
"Light Blue": "member_variable_color",
"Light Magenta": "code_folding_color",
"Light Cyan": "user_type_color",
"White": "text_selected_color",
"Background": "background_color",
"Foreground": "caret_color",
}
)
_native_terminal._update_theme()

# In editor _process is not called continuously unless the "Update Continuously"
# editor setting is enabled. This setting is disabled by default and uses 100%
# of one core when enabled, so best to leave it off and use a timer instead.
add_child(timer)
timer.wait_time = 0.025
timer.connect("timeout", self, "_poll")
timer.start()


func _poll():
if pty and pty._pipe:
pty._pipe.poll()
update()


func _input(event):
if not has_focus():
return

# We need to handle many input events otherwise keys such as TAB, ctrl, etc.
# will trigger editor shortcuts when using them in the terminal.
if event is InputEventKey:
get_tree().set_input_as_handled()
_gui_input(event)
24 changes: 24 additions & 0 deletions addons/godot_xterm/editor_plugins/terminal/editor_terminal.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[gd_scene load_steps=3 format=2]

[ext_resource path="res://addons/godot_xterm/editor_plugins/terminal/editor_terminal.gd" type="Script" id=1]
[ext_resource path="res://addons/godot_xterm/nodes/pty/unix/pty_unix.gd" type="Script" id=2]

[node name="Terminal" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
focus_mode = 1
size_flags_horizontal = 4
size_flags_vertical = 4
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
copy_on_selection = true

[node name="PTY" type="Node" parent="."]
script = ExtResource( 2 )
terminal_path = NodePath("..")
env = {
"COLORTERM": "truecolor",
"TERM": "xterm-256color"
}
110 changes: 110 additions & 0 deletions addons/godot_xterm/editor_plugins/terminal/terminal_panel.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Copyright (c) 2021, Leroy Hopson (MIT License).
#
# This file contains snippets of code derived from Godot's editor_node.cpp file.
# These snippets are copyright of their authors and released under the MIT license:
# - Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur (MIT License).
# - Copyright (c) 2014-2021 Godot Engine contributors (MIT License).
tool
extends Control

const EditorTerminal := preload("./editor_terminal.tscn")

# Has access to the EditorSettings singleton so it can dynamically generate the
# terminal color scheme based on editor theme settings.
var editor_interface: EditorInterface

onready var editor_settings: EditorSettings = editor_interface.get_editor_settings()
onready var tabs: Tabs = $VBoxContainer/TabbarContainer/Tabs
onready var tabbar_container: HBoxContainer = $VBoxContainer/TabbarContainer
onready var add_button: ToolButton = $VBoxContainer/TabbarContainer/Tabs/AddButton
onready var tab_container: TabContainer = $VBoxContainer/TabContainer
onready var ready := true

var _theme := Theme.new()
var _tab_container_min_size


func _ready():
tab_container.add_stylebox_override("panel", get_stylebox("Background", "EditorStyles"))
_update_settings()


func _update_settings() -> void:
var editor_scale: float = editor_interface.get_editor_scale()
rect_min_size = Vector2(0, tabbar_container.rect_size.y + 182) * editor_scale

# Use the same policy as editor scene tabs.
tabs.tab_close_display_policy = (
Tabs.CLOSE_BUTTON_SHOW_ALWAYS
if editor_settings.get_setting("interface/scene_tabs/always_show_close_button")
else Tabs.CLOSE_BUTTON_SHOW_ACTIVE_ONLY
)

_update_terminal_tabs()


func _update_terminal_tabs():
# Wait a couple of frames to allow everything to resize before updating.
yield(get_tree(), "idle_frame")
yield(get_tree(), "idle_frame")

if tabs.get_offset_buttons_visible():
# Move add button to fixed position on the tabbar.
if add_button.get_parent() == tabs:
add_button.rect_position = Vector2.ZERO
tabs.remove_child(add_button)
tabbar_container.add_child(add_button)
tabbar_container.move_child(add_button, 0)
else:
# Move add button after last tab.
if add_button.get_parent() == tabbar_container:
tabbar_container.remove_child(add_button)
tabs.add_child(add_button)
var last_tab := Rect2()
if tabs.get_tab_count() > 0:
last_tab = tabs.get_tab_rect(tabs.get_tab_count() - 1)
add_button.rect_position = Vector2(
last_tab.position.x + last_tab.size.x + 3, last_tab.position.y
)

# Make sure we still own the button, so it gets saved with our scene.
add_button.owner = self


func _on_AddButton_pressed():
var shell = OS.get_environment("SHELL") if OS.has_environment("SHELL") else "sh"
var terminal := EditorTerminal.instance()
tabs.add_tab(shell.get_file())
terminal.editor_settings = editor_settings
terminal.set_anchors_preset(PRESET_WIDE)
tab_container.add_child(terminal)
terminal.pty.fork(shell)
terminal.grab_focus()
tabs.current_tab = tabs.get_tab_count() - 1
tab_container.current_tab = tabs.current_tab
_update_terminal_tabs()


func _on_Tabs_tab_changed(tab_index):
tab_container.current_tab = tab_index
tab_container.get_child(tab_index).grab_focus()


func _on_Tabs_tab_close(tab_index):
tabs.remove_tab(tab_index)
tab_container.get_child(tab_index).queue_free()
_update_terminal_tabs()


func _notification(what):
if not ready:
return

match what:
EditorSettings.NOTIFICATION_EDITOR_SETTINGS_CHANGED:
_update_settings()
_update_terminal_tabs()
NOTIFICATION_RESIZED:
_update_terminal_tabs()
NOTIFICATION_WM_FOCUS_IN:
_update_terminal_tabs()
100 changes: 100 additions & 0 deletions addons/godot_xterm/editor_plugins/terminal/terminal_panel.tscn
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
[gd_scene load_steps=7 format=2]

[ext_resource path="res://addons/godot_xterm/editor_plugins/terminal/terminal_panel.gd" type="Script" id=1]

[sub_resource type="Image" id=6]
data = {
"data": PoolByteArray( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ),
"format": "LumAlpha8",
"height": 16,
"mipmaps": false,
"width": 16
}

[sub_resource type="ImageTexture" id=2]
flags = 4
flags = 4
image = SubResource( 6 )
size = Vector2( 16, 16 )

[sub_resource type="StyleBoxTexture" id=3]
texture = SubResource( 2 )
region_rect = Rect2( 0, 0, 16, 16 )
margin_left = 2.0
margin_right = 2.0
margin_top = 2.0
margin_bottom = 2.0

[sub_resource type="Image" id=7]
data = {
"data": PoolByteArray( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 224, 224, 0, 224, 224, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}

[sub_resource type="ImageTexture" id=5]
flags = 0
flags = 0
image = SubResource( 7 )
size = Vector2( 16, 16 )

[node name="Panel" type="Panel"]
anchor_right = 1.0
anchor_bottom = 1.0
margin_top = -3.0
rect_min_size = Vector2( 0, 34 )
custom_styles/panel = SubResource( 3 )
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}

[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
rect_min_size = Vector2( 0, 24 )
custom_constants/separation = 0
__meta__ = {
"_edit_use_anchors_": false
}

[node name="TabbarContainer" type="HBoxContainer" parent="VBoxContainer"]
margin_right = 1024.0
margin_bottom = 24.0

[node name="Tabs" type="Tabs" parent="VBoxContainer/TabbarContainer"]
margin_right = 1024.0
margin_bottom = 24.0
size_flags_horizontal = 3
tab_align = 0
tab_close_display_policy = 1
__meta__ = {
"_edit_use_anchors_": false
}

[node name="AddButton" type="ToolButton" parent="VBoxContainer/TabbarContainer/Tabs"]
margin_left = 3.0
margin_right = 31.0
margin_bottom = 24.0
hint_tooltip = "Add a new scene."
icon = SubResource( 5 )
__meta__ = {
"_edit_use_anchors_": false
}

[node name="TabContainer" type="TabContainer" parent="VBoxContainer"]
margin_top = 24.0
margin_right = 1024.0
margin_bottom = 603.0
rect_clip_content = true
size_flags_vertical = 3
custom_styles/panel = SubResource( 3 )
custom_constants/top_margin = 0
custom_constants/side_margin = 0
tabs_visible = false

[connection signal="tab_changed" from="VBoxContainer/TabbarContainer/Tabs" to="." method="_on_Tabs_tab_changed"]
[connection signal="tab_close" from="VBoxContainer/TabbarContainer/Tabs" to="." method="_on_Tabs_tab_close"]
[connection signal="pressed" from="VBoxContainer/TabbarContainer/Tabs/AddButton" to="." method="_on_AddButton_pressed"]
2 changes: 1 addition & 1 deletion addons/godot_xterm/native/godotxtermnative.gdnlib
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
singleton=false
load_once=true
symbol_prefix="godot_"
reloadable=true
reloadable=false

[entry]

Expand Down
1 change: 1 addition & 0 deletions addons/godot_xterm/nodes/terminal/terminal.gd
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ func _ready():


func _refresh():
_screen.update()
if update_mode == UpdateMode.AUTO:
_native_terminal.update_mode = UpdateMode.ALL_NEXT_FRAME

Expand Down
6 changes: 6 additions & 0 deletions addons/godot_xterm/plugin.gd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ extends EditorPlugin

var pty_supported := OS.get_name() in ["X11", "Server", "OSX"]
var asciicast_import_plugin
var terminal_panel: Control


func _enter_tree():
Expand All @@ -23,6 +24,9 @@ func _enter_tree():
"X11", "Server", "OSX":
pty_script = load("res://addons/godot_xterm/nodes/pty/unix/pty_unix.gd")
add_custom_type("PTY", "Node", pty_script, pty_icon)
terminal_panel = preload("./editor_plugins/terminal/terminal_panel.tscn").instance()
terminal_panel.editor_interface = get_editor_interface()
add_control_to_bottom_panel(terminal_panel, "Terminal")


func _exit_tree():
Expand All @@ -34,3 +38,5 @@ func _exit_tree():

if pty_supported:
remove_custom_type("PTY")
remove_control_from_bottom_panel(terminal_panel)
terminal_panel.free()

0 comments on commit 5e25ebb

Please sign in to comment.