2023-09-06 16:04:23 +02:00
class_name Terminal
2020-09-01 21:25:24 +02:00
extends Control
2020-06-10 18:25:41 +02:00
2023-09-06 16:04:23 +02:00
signal command_done_signal
2020-09-30 21:36:11 +02:00
2020-09-01 19:59:07 +02:00
var history_position = 0
2020-10-06 18:42:28 +02:00
var git_commands = [ " add " , " am " , " archive " , " bisect " , " branch " , " bundle " , " checkout " , " cherry-pick " , " citool " , " clean " , " clone " , " commit " , " describe " , " diff " , " fetch " , " format-patch " , " gc " , " gitk " , " grep " , " gui " , " init " , " log " , " merge " , " mv " , " notes " , " pull " , " push " , " range-diff " , " rebase " , " reset " , " restore " , " revert " , " rm " , " shortlog " , " show " , " sparse-checkout " , " stash " , " status " , " submodule " , " switch " , " tag " , " worktree " , " config " , " fast-export " , " fast-import " , " filter-branch " , " mergetool " , " pack-refs " , " prune " , " reflog " , " remote " , " repack " , " replace " , " annotate " , " blame " , " bugreport " , " count-objects " , " difftool " , " fsck " , " gitweb " , " help " , " instaweb " , " merge-tree " , " rerere " , " show-branch " , " verify-commit " , " verify-tag " , " whatchanged " , " archimport " , " cvsexportcommit " , " cvsimport " , " cvsserver " , " imap-send " , " p " , " quiltimport " , " request-pull " , " send-email " , " svn " , " apply " , " checkout-index " , " commit-graph " , " commit-tree " , " hash-object " , " index-pack " , " merge-file " , " merge-index " , " mktag " , " mktree " , " multi-pack-index " , " pack-objects " , " prune-packed " , " read-tree " , " symbolic-ref " , " unpack-objects " , " update-index " , " update-ref " , " write-tree " , " cat-file " , " cherry " , " diff-files " , " diff-index " , " diff-tree " , " for-each-ref " , " get-tar-commit-id " , " ls-files " , " ls-remote " , " ls-tree " , " merge-base " , " name-rev " , " pack-redundant " , " rev-list " , " rev-parse " , " show-index " , " show-ref " , " unpack-file " , " var " , " verify-pack " , " daemon " , " fetch-pack " , " http-backend " , " send-pack " , " update-server-info " , " check-attr " , " check-ignore " , " check-mailmap " , " check-ref-format " , " column " , " credential " , " credential-cache " , " credential-store " , " fmt-merge-msg " , " interpret-trailers " , " mailinfo " , " mailsplit " , " merge-one-file " , " patch-id " , " sh-i " , " sh-setup " ]
2020-09-28 17:39:16 +02:00
var git_commands_help = [ ]
2020-09-01 19:59:07 +02:00
2023-09-06 16:04:23 +02:00
@ onready var input = $ Rows / InputLine / Input
@ onready var output = $ Rows / TopHalf / Output
@ onready var completions = $ Rows / TopHalf / Completions
2020-09-30 21:36:11 +02:00
var repository
2023-09-06 16:04:23 +02:00
@ onready var main = get_tree ( ) . get_root ( ) . get_node ( " Main " )
2020-09-01 21:25:24 +02:00
2023-09-07 14:45:52 +02:00
var shell = await Shell . new ( )
2021-03-04 14:49:16 +01:00
2020-09-14 19:25:57 +02:00
var premade_commands = [
' git commit --allow-empty -m " empty " ' ,
' echo $RANDOM | git hash-object -w --stdin ' ,
' git switch -c $RANDOM ' ,
]
2020-09-08 22:16:18 +02:00
func _ready ( ) :
2023-09-06 16:04:23 +02:00
var error = $ TextEditor . connect ( " hidden " , Callable ( self , " editor_closed " ) )
2020-09-24 11:15:00 +02:00
if error != OK :
2020-09-29 14:53:00 +02:00
helpers . crash ( " Could not connect TextEditor ' s hide signal " )
2020-09-22 21:56:22 +02:00
input . grab_focus ( )
2020-10-06 18:42:28 +02:00
for subcommand in git_commands :
git_commands_help . push_back ( " " )
2020-09-28 17:39:16 +02:00
2020-09-27 21:50:14 +02:00
completions . hide ( )
2020-09-28 16:18:06 +02:00
history_position = game . state [ " history " ] . size ( )
2020-09-14 19:25:57 +02:00
2020-09-21 20:28:43 +02:00
func _input ( event ) :
2020-11-10 22:42:43 +01:00
if not input . has_focus ( ) :
return
2020-09-28 16:18:06 +02:00
if game . state [ " history " ] . size ( ) > 0 :
2020-09-21 20:28:43 +02:00
if event . is_action_pressed ( " ui_up " ) :
if history_position > 0 :
history_position -= 1
2020-09-28 16:18:06 +02:00
input . text = game . state [ " history " ] [ history_position ]
2023-09-06 16:04:23 +02:00
input . caret_column = input . text . length ( )
2020-09-21 20:28:43 +02:00
# This prevents the Input taking the arrow as a "skip to beginning" command.
2023-09-06 16:04:23 +02:00
get_viewport ( ) . set_input_as_handled ( )
2020-09-21 20:28:43 +02:00
if event . is_action_pressed ( " ui_down " ) :
2020-09-28 16:18:06 +02:00
if history_position < game . state [ " history " ] . size ( ) - 1 :
2020-09-21 20:28:43 +02:00
history_position += 1
2020-09-28 16:18:06 +02:00
input . text = game . state [ " history " ] [ history_position ]
2023-09-06 16:04:23 +02:00
input . caret_column = input . text . length ( )
get_viewport ( ) . set_input_as_handled ( )
2020-09-28 17:10:21 +02:00
if event . is_action_pressed ( " tab_complete " ) :
if completions . visible :
completions . get_root ( ) . get_children ( ) . select ( 0 )
2023-09-06 16:04:23 +02:00
get_viewport ( ) . set_input_as_handled ( )
2020-10-06 11:51:10 +02:00
if event . is_action_pressed ( " delete_word " ) :
2023-09-06 16:04:23 +02:00
var first_half = input . text . substr ( 0 , input . caret_column )
var second_half = input . text . substr ( input . caret_column )
2020-10-06 11:51:10 +02:00
2023-09-06 16:04:23 +02:00
var idx = first_half . strip_edges ( false , true ) . rfind ( " " )
2020-10-06 11:51:10 +02:00
if idx > 0 :
input . text = first_half . substr ( 0 , idx + 1 ) + second_half
2023-09-06 16:04:23 +02:00
input . caret_column = idx + 1
2020-10-06 11:51:10 +02:00
else :
input . text = " " + second_half
2020-10-27 19:44:17 +01:00
if event . is_action_pressed ( " clear " ) :
clear ( )
2020-09-28 17:10:21 +02:00
2020-09-14 19:25:57 +02:00
func load_command ( id ) :
input . text = premade_commands [ id ]
2023-09-06 16:04:23 +02:00
input . caret_column = input . text . length ( )
2020-09-14 19:25:57 +02:00
2020-09-01 14:03:18 +02:00
func send_command ( command ) :
2020-11-10 22:52:48 +01:00
close_all_editors ( )
2020-09-28 16:18:06 +02:00
game . state [ " history " ] . push_back ( command )
game . save_state ( )
history_position = game . state [ " history " ] . size ( )
2020-09-01 19:59:07 +02:00
2020-09-11 10:33:44 +02:00
input . editable = false
2020-09-27 21:50:14 +02:00
completions . hide ( )
2020-09-08 22:16:18 +02:00
2021-02-11 11:05:33 +01:00
# If someone tries to run an editor, use fake-editor instead.
var editor_regex = RegEx . new ( )
editor_regex . compile ( " ^(vim?|gedit|emacs|kate|nano|code) " )
command = editor_regex . sub ( command , " fake-editor " )
2021-03-04 14:49:16 +01:00
shell . cd ( repository . path )
2023-09-07 10:14:07 +02:00
2023-09-07 14:45:52 +02:00
print ( " running " + command )
2023-09-07 11:43:51 +02:00
var cmd = shell . run_async_web ( command , false )
await cmd . done
call_deferred ( " command_done " , cmd )
2020-11-09 19:37:26 +01:00
func command_done ( cmd ) :
2020-11-02 13:13:04 +01:00
if cmd . exit_code == 0 :
2023-09-06 16:04:23 +02:00
$ OkSound . pitch_scale = randf_range ( 0.8 , 1.2 )
2020-10-22 16:19:22 +02:00
$ OkSound . play ( )
else :
$ ErrorSound . play ( )
2020-09-01 21:25:24 +02:00
input . text = " "
2020-09-11 12:23:26 +02:00
input . editable = true
2020-09-29 17:05:04 +02:00
2020-11-02 13:13:04 +01:00
if cmd . output . length ( ) < = 1000 :
2020-11-09 19:37:26 +01:00
output . text = output . text + " $ " + cmd . command + " \n " + cmd . output
2021-01-19 12:48:16 +01:00
game . notify ( " This is your terminal! All commands are executed here, and you can see their output. You can also type your own commands here! " , self , " terminal " )
2020-09-29 17:05:04 +02:00
else :
2020-11-02 13:13:04 +01:00
$ Pager / Text . text = cmd . output
2020-09-29 17:05:04 +02:00
$ Pager . popup ( )
2020-09-30 21:36:11 +02:00
2023-09-06 16:04:23 +02:00
emit_signal ( " command_done_signal " )
2020-11-02 13:13:04 +01:00
2020-09-08 22:16:18 +02:00
func receive_output ( text ) :
output . text += text
2020-09-24 10:10:14 +02:00
repository . update_everything ( )
2020-09-14 15:35:30 +02:00
func clear ( ) :
output . text = " "
2020-09-10 12:03:46 +02:00
2020-09-21 18:59:55 +02:00
func editor_closed ( ) :
input . grab_focus ( )
2020-09-25 12:04:45 +02:00
func regenerate_completions_menu ( new_text ) :
2023-09-07 14:45:52 +02:00
var comp = await generate_completions ( new_text )
2020-09-25 12:04:45 +02:00
completions . clear ( )
2020-10-06 12:30:24 +02:00
2020-09-25 12:04:45 +02:00
var filtered_comp = [ ]
for c in comp :
if c != new_text :
filtered_comp . push_back ( c )
if filtered_comp . size ( ) == 0 :
completions . hide ( )
else :
completions . show ( )
var _root = completions . create_item ( )
for c in filtered_comp :
var child = completions . create_item ( )
child . set_text ( 0 , c )
2020-09-28 17:39:16 +02:00
if c . split ( " " ) . size ( ) > = 2 :
var subcommand = c . split ( " " ) [ 1 ]
var idx = git_commands . find ( subcommand )
if idx > = 0 :
child . set_text ( 1 , git_commands_help [ idx ] )
2020-10-06 12:30:24 +02:00
2023-09-06 16:04:23 +02:00
completions . offset_top = - min ( filtered_comp . size ( ) * 35 + 10 , 210 )
2020-09-25 12:04:45 +02:00
2020-09-28 16:00:42 +02:00
func relevant_subcommands ( ) :
var result = { }
2020-09-28 16:18:06 +02:00
for h in game . state [ " history " ] :
2020-09-28 16:00:42 +02:00
var parts = Array ( h . split ( " " ) )
2020-11-02 15:39:12 +01:00
if parts . size ( ) > = 2 and parts [ 0 ] == " git " :
2020-09-28 16:00:42 +02:00
var subcommand = parts [ 1 ]
if git_commands . has ( subcommand ) :
if not result . has ( subcommand ) :
result [ subcommand ] = 0
result [ subcommand ] += 1
# Convert to format [["add", 3], ["pull", 5]].
var result_array = [ ]
for r in result :
result_array . push_back ( [ r , result [ r ] ] )
2023-09-06 16:04:23 +02:00
result_array . sort_custom ( Callable ( self , " sort_by_frequency_desc " ) )
2020-09-28 16:00:42 +02:00
var plain_result = [ ]
for r in result_array :
plain_result . push_back ( r [ 0 ] )
return plain_result
func sort_by_frequency_desc ( a , b ) :
return a [ 1 ] > b [ 1 ]
2020-09-25 12:04:45 +02:00
func generate_completions ( command ) :
2020-09-28 16:52:00 +02:00
var results = [ ]
# Collect git commands.
2020-09-25 12:04:45 +02:00
if command . substr ( 0 , 4 ) == " git " :
var rest = command . substr ( 4 )
2020-09-28 16:00:42 +02:00
var subcommands = relevant_subcommands ( )
2020-09-25 12:04:45 +02:00
for sc in subcommands :
if sc . substr ( 0 , rest . length ( ) ) == rest :
results . push_back ( " git " + sc )
2020-09-28 16:52:00 +02:00
# Part 1: Only autocomplete after git subcommand.
# Part2: Prevent autocompletion to only show filename at the beginning of a command.
if ! ( command . substr ( 0 , 4 ) == " git " and command . split ( " " ) . size ( ) < = 2 ) and command . split ( " " ) . size ( ) > 1 :
var last_word = Array ( command . split ( " " ) ) . pop_back ( )
2023-09-07 12:04:47 +02:00
var file_string = await repository . shell . run ( " find . -type f " )
2020-09-28 16:52:00 +02:00
var files = file_string . split ( " \n " )
files = Array ( files )
# The last entry is an empty string, remove it.
files . pop_back ( )
for file_path in files :
file_path = file_path . substr ( 2 )
if file_path . substr ( 0 , 4 ) != " .git " and file_path . substr ( 0 , last_word . length ( ) ) == last_word :
results . push_back ( command + file_path . substr ( last_word . length ( ) ) )
return results
2020-09-25 12:04:45 +02:00
func _input_changed ( new_text ) :
call_deferred ( " regenerate_completions_menu " , new_text )
func _completion_selected ( ) :
var item = completions . get_selected ( )
input . text = item . get_text ( 0 )
input . emit_signal ( " text_changed " , input . text )
#completions.hide()
input . grab_focus ( )
2023-09-06 16:04:23 +02:00
input . caret_column = input . text . length ( )
2020-10-06 10:38:31 +02:00
func editor_saved ( ) :
emit_signal ( " command_done " )
2020-11-10 22:52:48 +01:00
func close_all_editors ( ) :
for editor in get_tree ( ) . get_nodes_in_group ( " editors " ) :
editor . close ( )