diff --git a/project.godot b/project.godot index 1cd98d5..ff0cb99 100644 --- a/project.godot +++ b/project.godot @@ -10,6 +10,11 @@ config_version=4 _global_script_classes=[ { "base": "Node", +"class": "BetterShell", +"language": "GDScript", +"path": "res://scenes/better_shell.gd" +}, { +"base": "Node", "class": "Chapter", "language": "GDScript", "path": "res://scenes/chapter.gd" @@ -40,6 +45,7 @@ _global_script_classes=[ { "path": "res://scenes/shell_command.gd" } ] _global_script_class_icons={ +"BetterShell": "", "Chapter": "", "FileBrowserItem": "", "Level": "", diff --git a/scenes/better_shell.gd b/scenes/better_shell.gd new file mode 100644 index 0000000..34b9ef8 --- /dev/null +++ b/scenes/better_shell.gd @@ -0,0 +1,75 @@ +extends Node +class_name BetterShell + +var exit_code + +var _cwd +var _os = OS.get_name() + +func _init(): + # Create required directories and move into the tmp directory. + _cwd = "/tmp" + run("mkdir -p '%s/repos'" % game.tmp_prefix) + _cwd = game.tmp_prefix + +func cd(dir): + _cwd = dir + +# Run a shell command given as a string. Run this if you're interested in the +# output of the command. +func run(command, crash_on_fail=true): + var shell_command = ShellCommand.new() + shell_command.command = command + shell_command.crash_on_fail = crash_on_fail + + run_async_thread(shell_command) + exit_code = shell_command.exit_code + return shell_command.output + +func run_async(command, crash_on_fail=true): + var shell_command = ShellCommand.new() + shell_command.command = command + shell_command.crash_on_fail = crash_on_fail + + var t = Thread.new() + shell_command.thread = t + t.start(self, "run_async_thread", shell_command) + + return shell_command + +func run_async_thread(shell_command): + var debug = false + + var command = shell_command.command + var crash_on_fail = shell_command.crash_on_fail + + if debug: + print("$ %s" % command) + + var env = {} + env["HOME"] = game.tmp_prefix + + var hacky_command = "" + for variable in env: + hacky_command += "export %s='%s';" % [variable, env[variable]] + + #hacky_command += "export PATH=\'"+game.tmp_prefix+":'\"$PATH\";" + hacky_command += "cd '%s' || exit 1;" % _cwd + hacky_command += command + + #print(hacky_command) + + var result + var shell_command_internal = game.shell_test(hacky_command) + + shell_command.output = shell_command_internal.output + shell_command.exit_code = shell_command_internal.exit_code + shell_command.emit_signal("done") + +func _shell_binary(): + if _os == "X11" or _os == "OSX": + return "bash" + elif _os == "Windows": + return "dependencies\\windows\\git\\bin\\bash.exe" + else: + helpers.crash("Unsupported OS: %s" % _os) diff --git a/scenes/game.gd b/scenes/game.gd index 8e63dd9..16db4c9 100644 --- a/scenes/game.gd +++ b/scenes/game.gd @@ -28,7 +28,7 @@ func _ready(): start_remote_shell() yield(get_tree().create_timer(0.1), "timeout") - global_shell = Shell.new() + global_shell = new_shell() # var cmd = global_shell.run("echo hi") # print(cmd) @@ -138,7 +138,6 @@ func toggle_music(): else: music.volume_db += 100 - func shell_test(command): mutex.lock() #print("go") @@ -150,3 +149,9 @@ func shell_test(command): #print("stop") mutex.unlock() return response + +func new_shell(): + if OS.get_name() == "Windows": + return BetterShell.new() + else: + return Shell.new() diff --git a/scenes/repository.gd b/scenes/repository.gd index d36a513..916bb03 100644 --- a/scenes/repository.gd +++ b/scenes/repository.gd @@ -14,7 +14,7 @@ var type = "remote" var node = preload("res://scenes/node.tscn") -var shell = Shell.new() +var shell = game.new_shell() var objects = {} var mouse_inside = false var has_been_layouted = false diff --git a/scenes/shell.gd b/scenes/shell.gd index 9039cd4..e406540 100644 --- a/scenes/shell.gd +++ b/scenes/shell.gd @@ -53,44 +53,44 @@ func run_async_thread(shell_command): for variable in env: hacky_command += "export %s='%s';" % [variable, env[variable]] - #hacky_command += "export PATH=\'"+game.tmp_prefix+":'\"$PATH\";" + hacky_command += "export PATH=\'"+game.tmp_prefix+":'\"$PATH\";" hacky_command += "cd '%s' || exit 1;" % _cwd hacky_command += command - #print(hacky_command) - var result - var shell_command_internal = game.shell_test(hacky_command) -# if _os == "X11" or _os == "OSX": -# # Godot's OS.execute wraps each argument in double quotes before executing -# # on Linux and macOS. -# # Because we want to be in a single-quote context, where nothing is evaluated, -# # we end those double quotes and start a single quoted string. For each single -# # quote appearing in our string, we close the single quoted string, and add -# # a double quoted string containing the single quote. Ooooof! -# # -# # Example: The string -# # -# # test 'fu' "bla" blubb -# # -# # becomes -# # -# # "'test '"'"'fu'"'"' "bla" blubb" -# -# hacky_command = '"\''+hacky_command.replace("'", "'\"'\"'")+'\'"' -# result = helpers.exec(_shell_binary(), ["-c", hacky_command], crash_on_fail) -# elif _os == "Windows": -# # On Windows, if the command contains a newline (even if inside a string), -# # execution will end. To avoid that, we first write the command to a file, -# # and run that file with bash. -# var script_path = game.tmp_prefix + "command" + str(randi()) -# helpers.write_file(script_path, hacky_command) -# result = helpers.exec(_shell_binary(), [script_path], crash_on_fail) -# else: -# helpers.crash("Unimplemented OS: %s" % _os) + if _os == "X11" or _os == "OSX": + # Godot's OS.execute wraps each argument in double quotes before executing + # on Linux and macOS. + # Because we want to be in a single-quote context, where nothing is evaluated, + # we end those double quotes and start a single quoted string. For each single + # quote appearing in our string, we close the single quoted string, and add + # a double quoted string containing the single quote. Ooooof! + # + # Example: The string + # + # test 'fu' "bla" blubb + # + # becomes + # + # "'test '"'"'fu'"'"' "bla" blubb" + + hacky_command = '"\''+hacky_command.replace("'", "'\"'\"'")+'\'"' + result = helpers.exec(_shell_binary(), ["-c", hacky_command], crash_on_fail) + elif _os == "Windows": + # On Windows, if the command contains a newline (even if inside a string), + # execution will end. To avoid that, we first write the command to a file, + # and run that file with bash. + var script_path = game.tmp_prefix + "command" + str(randi()) + helpers.write_file(script_path, hacky_command) + result = helpers.exec(_shell_binary(), [script_path], crash_on_fail) + else: + helpers.crash("Unimplemented OS: %s" % _os) - shell_command.output = shell_command_internal.output - shell_command.exit_code = shell_command_internal.exit_code + if debug: + print(result["output"]) + + shell_command.output = result["output"] + shell_command.exit_code = result["exit_code"] shell_command.emit_signal("done") func _shell_binary(): diff --git a/scenes/terminal.gd b/scenes/terminal.gd index 4eda373..5df56af 100644 --- a/scenes/terminal.gd +++ b/scenes/terminal.gd @@ -13,6 +13,8 @@ onready var completions = $Rows/TopHalf/Completions var repository onready var main = get_tree().get_root().get_node("Main") +var shell = Shell.new() + var premade_commands = [ 'git commit --allow-empty -m "empty"', 'echo $RANDOM | git hash-object -w --stdin', @@ -85,9 +87,9 @@ func send_command(command): editor_regex.compile("^(vim?|gedit|emacs|kate|nano|code) ") command = editor_regex.sub(command, "fake-editor ") -# var cmd = repository.shell.run_async(command, false) -# yield(cmd, "done") - var cmd = game.shell_test(command) + shell.cd(repository.path) + var cmd = shell.run_async(command, false) + yield(cmd, "done") call_deferred("command_done", cmd) func command_done(cmd):