Move all .tscn and .gd files into scenes/ directory

This commit is contained in:
Sebastian Morr 2020-10-26 19:15:47 +01:00
parent c330524f8e
commit 61304803bc
38 changed files with 88 additions and 77 deletions

36
scenes/arrow.gd Normal file
View file

@ -0,0 +1,36 @@
extends Node2D
var source: String
var target: String
var repository: Control
func _ready():
pass
func _process(_delta):
position = Vector2(0,0)
if not (repository and repository.objects.has(source)):
return
var start = repository.objects[source].position
var end = start + Vector2(0, 60)
if repository and repository.objects.has(target) and repository.objects[target].visible:
var t = repository.objects[target]
end = t.position
$Target.hide()
else:
$Target.text = target
if $Target.text.substr(0, 5) != "refs/":
$Target.text = ""
$Target.show()
$Line.hide()
$Tip.hide()
$Line.points[1] = end - repository.objects[source].position
# Move the tip away from the object a bit.
$Line.points[1] -= $Line.points[1].normalized()*30
$Tip.position = $Line.points[1]
$Tip.rotation = PI+$Line.points[0].angle_to($Line.points[1])

60
scenes/arrow.tscn Normal file
View file

@ -0,0 +1,60 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
[ext_resource path="res://scenes/arrow.gd" type="Script" id=2]
[node name="Arrow" type="Node2D"]
show_behind_parent = true
script = ExtResource( 2 )
[node name="Line" type="Line2D" parent="."]
points = PoolVector2Array( -0.480499, -0.11055, 158.301, 0.581757 )
default_color = Color( 0.2, 0.2, 0.2, 1 )
[node name="Tip" type="Node2D" parent="."]
position = Vector2( 158.06, 0.290878 )
z_index = 1
[node name="Polygon" type="Polygon2D" parent="Tip"]
position = Vector2( -24.7164, -6.37881 )
z_index = -1
color = Color( 0.2, 0.2, 0.2, 1 )
polygon = PoolVector2Array( -8.50021, 20.4619, 36.1874, 8.44903, 0.869781, -21.8232 )
[node name="Polygon2" type="Polygon2D" parent="Tip"]
visible = false
position = Vector2( -9.66138, -2.89842 )
z_index = -1
color = Color( 0.2, 0.2, 0.2, 1 )
polygon = PoolVector2Array( -8.50021, 20.4619, 22.2526, 5.80623, 2.31131, -19.9012, -12.104, -23.7453, 4.95413, 1.72188, -21.9546, 16.1372 )
[node name="Label" type="Node2D" parent="."]
visible = false
position = Vector2( 102, 46 )
[node name="ID" type="Label" parent="Label"]
margin_left = -19.374
margin_top = -5.93085
margin_right = 20.626
margin_bottom = 8.06915
custom_fonts/font = ExtResource( 1 )
custom_colors/font_color = Color( 1, 1, 1, 1 )
text = "label"
align = 1
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Target" type="Label" parent="."]
margin_left = -230.84
margin_top = 42.1225
margin_right = 231.16
margin_bottom = 68.1225
custom_fonts/font = ExtResource( 1 )
custom_colors/font_color = Color( 0.356863, 0.356863, 0.356863, 1 )
text = "label"
align = 1
__meta__ = {
"_edit_use_anchors_": false
}

120
scenes/card.gd Normal file
View file

@ -0,0 +1,120 @@
extends Node2D
var hovered = false
var dragged = false
var drag_offset
export var arg_number = 0
export var command = "" setget set_command
export var description = "" setget set_description
export var energy = 0 setget set_energy
var _first_argument = null
var _home_position = null
var _home_rotation = null
onready var energy_label = $Sprite/Energy
func _ready():
set_process_unhandled_input(true)
set_energy(energy)
func _process(delta):
if game.energy >= energy:
energy_label.modulate = Color(0.5, 1, 0.5)
else:
energy_label.modulate = Color(1, 1, 1)
modulate = Color(1, 0.5, 0.5)
if dragged:
var mousepos = get_viewport().get_mouse_position()
global_position = mousepos - drag_offset
var target_scale = 1
if hovered and not dragged:
target_scale = 1.5
var speed = 5
scale = lerp(scale, Vector2(target_scale, target_scale), 10*delta)
func _unhandled_input(event):
if event is InputEventMouseButton:
if event.button_index == BUTTON_LEFT and event.pressed and hovered:
dragged = true
game.dragged_object = self
$PickupSound.play()
drag_offset = get_viewport().get_mouse_position() - global_position
get_tree().set_input_as_handled()
modulate.a = 0.5
elif event.button_index == BUTTON_LEFT and !event.pressed and dragged:
dragged = false
game.dragged_object = null
modulate.a = 1
if get_viewport().get_mouse_position().y < get_viewport().size.y/3*2:
if arg_number == 0 :
try_play($Label.text)
else:
move_back()
else:
move_back()
func _mouse_entered():
hovered = true
z_index = 1
func _mouse_exited():
hovered = false
z_index = 0
func set_command(new_command):
command = new_command
$Label.text = command
func set_description(new_description):
description = new_description
$Description.text = description
func set_energy(new_energy):
energy = new_energy
if energy_label:
energy_label.text = str(energy)
func move_back():
position = _home_position
rotation_degrees = _home_rotation
$ReturnSound.play()
func buuurn():
move_back()
func dropped_on(other):
var full_command = ""
match arg_number:
1:
var argument = other.id
if ($Label.text.begins_with("git checkout") or $Label.text.begins_with("git rebase")) and other.id.begins_with("refs/heads"):
argument = Array(other.id.split("/")).pop_back()
full_command = $Label.text + " " + argument
try_play(full_command)
# 2:
# if _first_argument:
# full_command = $Label.text + " " + _first_argument + " " + other.id
# $"../Terminal".send_command(full_command)
# buuurn()
# else:
# _first_argument = other.id
func try_play(command):
if game.energy >= energy:
$PlaySound.play()
var particles = preload("res://scenes/card_particles.tscn").instance()
particles.position = position
get_parent().add_child(particles)
$"../../..".terminal.send_command(command)
buuurn()
game.energy -= energy
else:
move_back()

136
scenes/card.tscn Normal file
View file

@ -0,0 +1,136 @@
[gd_scene load_steps=10 format=2]
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
[ext_resource path="res://nodes/blob.svg" type="Texture" id=2]
[ext_resource path="res://scenes/card.gd" type="Script" id=3]
[ext_resource path="res://sounds/swish.wav" type="AudioStream" id=4]
[ext_resource path="res://sounds/swoosh.wav" type="AudioStream" id=5]
[ext_resource path="res://sounds/poof.wav" type="AudioStream" id=6]
[sub_resource type="StyleBoxFlat" id=1]
bg_color = Color( 0.45098, 0.584314, 0.843137, 1 )
border_color = Color( 0.0627451, 0.141176, 0.176471, 1 )
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10
shadow_color = Color( 0, 0, 0, 0.392157 )
shadow_size = 4
shadow_offset = Vector2( -2, 2 )
[sub_resource type="RectangleShape2D" id=2]
extents = Vector2( 105.74, 143.46 )
[sub_resource type="StyleBoxFlat" id=3]
bg_color = Color( 1, 1, 1, 0.243137 )
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10
[node name="Card" type="Node2D" groups=[
"cards",
]]
script = ExtResource( 3 )
[node name="Panel" type="Panel" parent="."]
margin_left = -105.0
margin_top = -291.0
margin_right = 104.0
margin_bottom = -2.0
mouse_filter = 2
custom_styles/panel = SubResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="ColorRect" type="ColorRect" parent="."]
visible = false
margin_left = -103.0
margin_top = -290.336
margin_right = 103.0
margin_bottom = -1.33582
mouse_filter = 2
color = Color( 0.105882, 0.67451, 0.847059, 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Area2D" type="Area2D" parent="."]
position = Vector2( 0, -145.336 )
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
position = Vector2( -6.10352e-05, 0.00012207 )
shape = SubResource( 2 )
[node name="Label" type="Label" parent="."]
margin_left = -89.0
margin_top = -276.0
margin_right = 85.0
margin_bottom = -185.0
custom_fonts/font = ExtResource( 1 )
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "Name"
autowrap = true
__meta__ = {
"_edit_use_anchors_": false
}
[node name="ColorRect2" type="Panel" parent="."]
margin_left = -97.0
margin_top = -169.0
margin_right = 94.0
margin_bottom = -10.0
mouse_filter = 2
custom_styles/panel = SubResource( 3 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Description" type="Label" parent="."]
margin_left = -92.0
margin_top = -164.0
margin_right = 133.0
margin_bottom = 23.0
rect_scale = Vector2( 0.75, 0.75 )
custom_fonts/font = ExtResource( 1 )
custom_colors/font_color = Color( 0, 0, 0, 1 )
text = "Description"
autowrap = true
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Sprite" type="Sprite" parent="."]
visible = false
position = Vector2( -103.288, -287.778 )
scale = Vector2( 0.542341, 0.542341 )
texture = ExtResource( 2 )
[node name="Energy" type="Label" parent="Sprite"]
margin_left = -51.1637
margin_top = -47.4558
margin_right = -17.1637
margin_bottom = -16.4558
rect_scale = Vector2( 3, 3 )
custom_fonts/font = ExtResource( 1 )
text = "0"
align = 1
valign = 1
__meta__ = {
"_edit_use_anchors_": false
}
[node name="PickupSound" type="AudioStreamPlayer" parent="."]
stream = ExtResource( 4 )
[node name="PlaySound" type="AudioStreamPlayer" parent="."]
stream = ExtResource( 6 )
volume_db = -6.848
[node name="ReturnSound" type="AudioStreamPlayer" parent="."]
stream = ExtResource( 5 )
volume_db = -6.848
[connection signal="mouse_entered" from="Area2D" to="." method="_mouse_entered"]
[connection signal="mouse_exited" from="Area2D" to="." method="_mouse_exited"]

View file

@ -0,0 +1,67 @@
[gd_scene load_steps=5 format=2]
[sub_resource type="Curve" id=1]
_data = [ Vector2( 0, 1 ), 0.0, 0.0, 0, 0, Vector2( 1, 0 ), -2.75937, 0.0, 0, 0 ]
[sub_resource type="CurveTexture" id=2]
curve = SubResource( 1 )
[sub_resource type="ParticlesMaterial" id=3]
emission_shape = 2
emission_box_extents = Vector3( 100, 150, 1 )
flag_disable_z = true
spread = 180.0
gravity = Vector3( 0, 0, 0 )
initial_velocity = 232.55
initial_velocity_random = 0.52
orbit_velocity = 0.0
orbit_velocity_random = 0.0
scale = 14.95
scale_curve = SubResource( 2 )
color = Color( 0.223529, 0.592157, 0.772549, 1 )
[sub_resource type="Animation" id=4]
resource_name = "play"
length = 2.0
tracks/0/type = "method"
tracks/0/path = NodePath(".")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/keys = {
"times": PoolRealArray( 2 ),
"transitions": PoolRealArray( 1 ),
"values": [ {
"args": [ ],
"method": "queue_free"
} ]
}
tracks/1/type = "value"
tracks/1/path = NodePath("Particles:emitting")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/keys = {
"times": PoolRealArray( 0 ),
"transitions": PoolRealArray( 1 ),
"update": 1,
"values": [ true ]
}
[node name="CardParticles" type="Node2D"]
[node name="Particles" type="Particles2D" parent="."]
position = Vector2( -0.539337, -145.087 )
emitting = false
amount = 32
lifetime = 0.2
one_shot = true
explosiveness = 0.91
local_coords = false
process_material = SubResource( 3 )
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
autoplay = "play"
anims/play = SubResource( 4 )

224
scenes/cardgame.gd Normal file
View file

@ -0,0 +1,224 @@
extends Control
var cards = [
# {
# "command": 'git add .',
# "arg_number": 0,
# "description": "Add all files in the working directory to the index.",
# "energy": 1
# },
#
# {
# "command": 'touch "file$RANDOM"',
# "arg_number": 0,
# "description": "Create a new file.",
# "energy": 2
# },
# {
# "command": 'git checkout -b "$RANDOM"',
# "arg_number": 0,
# "description": "Create a new branch and switch to it.",
# "energy": 2
# },
# {
# "command": 'git merge',
# "arg_number": 1,
# "description": "Merge specified commit into HEAD.",
# "energy": 1
# },
# {
# "command": 'git commit --allow-empty -m "$RANDOM"',
# "arg_number": 0,
# "description": "Add a new commit under HEAD.",
# "energy": 1
# },
{
"command": 'git checkout',
"arg_number": 1,
"description": "Travel to a commit!",
#"description": "Point HEAD to a branch or commit, and update the index and the working directory.",
"energy": 1
},
{
"command": 'git add .; git commit',
"arg_number": 0,
"description": "Make a new commit!",
"energy": 1
},
# {
# "command": 'git branch new',
# "arg_number": 1,
# "description": "Create a new timeline.",
# "energy": 1
# },
{
"command": 'git merge',
"arg_number": 1,
"description": "Merge the specified timeline into yours.",
"energy": 1
},
{
"command": 'git rebase',
"arg_number": 1,
"description": "Put the events in your current timeline on top of the specified one.",
"energy": 1
},
{
"command": 'git pull',
"arg_number": 0,
"description": "Get timelines from a colleague.",
"energy": 1
},
{
"command": 'git push',
"arg_number": 0,
"description": "Give timelines to a colleague.",
"energy": 1
},
{
"command": 'git rebase -i',
"arg_number": 1,
"description": "Make changes to the events in your current timeline, back to the commit you drag this to.",
"energy": 1
},
{
"command": 'git reset --hard',
"arg_number": 1,
"description": "Reset current label to the specified commit.",
"energy": 1
},
{
"command": 'git cherry-pick',
"arg_number": 1,
"description": "Repeat the specified action on top of your current timeline.",
"energy": 1
},
# {
# "command": 'git update-ref -d',
# "arg_number": 1,
# "description": "Delete a ref.",
# "energy": 1
# },
# {
# "command": 'git reflog expire --expire=now --all; git prune',
# "arg_number": 0,
# "description": "Delete all unreferenced objects.",
# "energy": 1
# },
# {
# "command": 'git rebase',
# "arg_number": 1,
# "description": "Rebase current branch on top of specified commit.",
# "energy": 1
# },
# {
# "command": 'git push -f',
# "arg_number": 0,
# "description": "Push current branch to the remote, overwriting existing commits. Will make everyone angry.",
# "energy": 3
# },
# {
# "command": 'git pull',
# "arg_number": 0,
# "description": "Pull current branch from the remote.",
# "energy": 2
# },
]
func _ready():
# var path = game.tmp_prefix_inside+"/repos/sandbox/"
# helpers.careful_delete(path)
#
# game.global_shell.run("mkdir " + path)
# game.global_shell.cd(path)
# game.global_shell.run("git init")
# game.global_shell.run("git remote add origin ../remote")
# $Repository.path = path
# $Terminal.repository = $Repository
#
# var path2 = game.tmp_prefix_inside+"/repos/remote/"
# helpers.careful_delete(path2)
#
# game.global_shell.run("mkdir " + path2)
# game.global_shell.cd(path2)
# game.global_shell.run("git init")
# game.global_shell.run("git config receive.denyCurrentBranch ignore")
# $RepositoryRemote.path = path2
redraw_all_cards()
arrange_cards()
pass
func _process(delta):
if $Energy:
$Energy.text = str(game.energy)
#func _update_repo():
# $Repository.update_everything()
# $RepositoryRemote.update_everything()
func draw_rand_card():
var deck = []
for card in cards:
deck.push_back(card)
# We want a lot of commit and checkout cards!
for i in range(5):
deck.push_back(cards[0])
deck.push_back(cards[1])
var card = deck[randi() % deck.size()]
draw_card(card)
func draw_card(card):
var new_card = preload("res://scenes/card.tscn").instance()
new_card.command = card.command
new_card.arg_number = card.arg_number
new_card.description = card.description
new_card.energy = 0 #card.energy
new_card.position = Vector2(rect_size.x, rect_size.y*2)
add_child(new_card)
arrange_cards()
func arrange_cards():
var t = Timer.new()
t.wait_time = 0.05
add_child(t)
t.start()
yield(t, "timeout")
var amount_cards = get_tree().get_nodes_in_group("cards").size()
var total_angle = min(50, 45.0/7*amount_cards)
var angle_between_cards = 0
if amount_cards > 1:
angle_between_cards = total_angle / (amount_cards-1)
var current_angle = -total_angle/2
for card in get_tree().get_nodes_in_group("cards"):
var target_position = Vector2(rect_size.x/2, rect_size.y + 1500)
var target_rotation = current_angle
var translation_vec = Vector2(0,-1500).rotated(current_angle/180.0*PI)
target_position += translation_vec
current_angle += angle_between_cards
card._home_position = target_position
card._home_rotation = target_rotation
var tween = Tween.new()
tween.interpolate_property(card, "position", card.position, target_position, 0.5, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT)
tween.interpolate_property(card, "rotation_degrees", card.rotation_degrees, target_rotation, 0.5, Tween.TRANS_CUBIC, Tween.EASE_IN_OUT)
add_child(tween)
tween.start()
func redraw_all_cards():
game.energy = 5
for card in get_tree().get_nodes_in_group("cards"):
card.queue_free()
for card in cards:
draw_card(card)
func add_card(command):
draw_card({"command": command, "description": "", "arg_number": 0, "energy": 0})

65
scenes/cardgame.tscn Normal file
View file

@ -0,0 +1,65 @@
[gd_scene load_steps=6 format=2]
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
[ext_resource path="res://scenes/repository.tscn" type="PackedScene" id=3]
[ext_resource path="res://scenes/cardgame.gd" type="Script" id=4]
[ext_resource path="res://scenes/terminal.tscn" type="PackedScene" id=5]
[ext_resource path="res://fonts/big.tres" type="DynamicFont" id=6]
[node name="Cardgame" type="Node2D"]
script = ExtResource( 4 )
[node name="ColorRect" type="ColorRect" parent="."]
margin_right = 1922.0
margin_bottom = 1083.0
mouse_filter = 2
color = Color( 0.0823529, 0.0823529, 0.0823529, 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="RepositoryRemote" parent="." instance=ExtResource( 3 )]
margin_right = 748.0
margin_bottom = 746.0
label = "remote"
simplified_view = true
[node name="Repository" parent="." instance=ExtResource( 3 )]
margin_left = 762.0
margin_right = 1481.0
margin_bottom = 744.0
label = "yours"
simplified_view = true
[node name="Terminal" parent="." instance=ExtResource( 5 )]
margin_left = 1488.0
margin_top = 5.0
margin_right = 1914.0
margin_bottom = 745.0
[node name="Button" type="Button" parent="."]
margin_left = 1719.41
margin_top = 814.594
margin_right = 1892.41
margin_bottom = 856.594
custom_fonts/font = ExtResource( 1 )
text = "Draw new cards"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Energy" type="Label" parent="."]
modulate = Color( 0.431373, 0.792157, 0.423529, 1 )
margin_left = 35.3364
margin_top = 796.429
margin_right = 75.3364
margin_bottom = 845.429
rect_scale = Vector2( 2, 2 )
custom_fonts/font = ExtResource( 6 )
text = "3"
__meta__ = {
"_edit_use_anchors_": false
}
[connection signal="command_done" from="Terminal" to="." method="_update_repo"]
[connection signal="pressed" from="Button" to="." method="redraw_all_cards"]

43
scenes/cards.tscn Normal file
View file

@ -0,0 +1,43 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
[ext_resource path="res://fonts/big.tres" type="DynamicFont" id=2]
[ext_resource path="res://scenes/cardgame.gd" type="Script" id=3]
[node name="Cards" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
script = ExtResource( 3 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Button" type="Button" parent="."]
anchor_left = 1.0
anchor_right = 1.0
margin_left = -172.0
margin_top = 16.0
margin_right = -16.0
margin_bottom = 47.0
custom_fonts/font = ExtResource( 1 )
text = "Draw new cards"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Energy" type="Label" parent="."]
visible = false
modulate = Color( 0.431373, 0.792157, 0.423529, 1 )
margin_left = 28.2219
margin_top = 16.6766
margin_right = 68.2219
margin_bottom = 65.6766
rect_scale = Vector2( 2, 2 )
custom_fonts/font = ExtResource( 2 )
text = "3"
__meta__ = {
"_edit_use_anchors_": false
}
[connection signal="pressed" from="Button" to="." method="redraw_all_cards"]

49
scenes/chapter.gd Normal file
View file

@ -0,0 +1,49 @@
extends Node
class_name Chapter
var slug
var levels
# Path is an outer path.
func load(path):
levels = []
var parts = path.split("/")
slug = parts[parts.size()-1]
var level_names = []
var dir = Directory.new()
dir.open("res://levels/%s" % slug)
dir.list_dir_begin()
while true:
var file = dir.get_next()
if file == "":
break
elif not file.begins_with(".") and file != "sequence":
level_names.append(file)
dir.list_dir_end()
level_names.sort()
var final_level_sequence = []
var level_sequence = Array(helpers.read_file("res://levels/%s/sequence" % slug, "").split("\n"))
for level in level_sequence:
if level == "":
continue
if not level_names.has(level):
helpers.crash("Level '%s' is specified in the sequence, but could not be found" % level)
level_names.erase(level)
final_level_sequence.push_back(level)
final_level_sequence += level_names
for l in final_level_sequence:
var level = Level.new()
level.load("res://levels/%s/%s" % [slug, l])
levels.push_back(level)
func _to_string():
return str(levels)

19
scenes/drop_area.gd Normal file
View file

@ -0,0 +1,19 @@
extends Node2D
var hovered = false
func _ready():
pass
func _mouse_entered():
hovered = true
func _mouse_exited():
hovered = false
func _input(event):
if event is InputEventMouseButton:
if event.button_index == BUTTON_LEFT and !event.pressed and hovered:
if game.dragged_object:
print(game.dragged_object)
game.dragged_object.dropped_on($"..")

18
scenes/drop_area.tscn Normal file
View file

@ -0,0 +1,18 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://scenes/drop_area.gd" type="Script" id=1]
[sub_resource type="CircleShape2D" id=1]
radius = 23.5871
[node name="DropArea" type="Node2D"]
position = Vector2( -0.197731, 0.0673599 )
script = ExtResource( 1 )
[node name="Area2D" type="Area2D" parent="."]
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
shape = SubResource( 1 )
[connection signal="mouse_entered" from="Area2D" to="." method="_mouse_entered"]
[connection signal="mouse_exited" from="Area2D" to="." method="_mouse_exited"]

133
scenes/file_browser.gd Normal file
View file

@ -0,0 +1,133 @@
extends Control
enum FileBrowserMode {
WORKING_DIRECTORY,
COMMIT,
INDEX
}
export(String) var title setget _set_title
export(FileBrowserMode) var mode = FileBrowserMode.WORKING_DIRECTORY setget _set_mode
var shell
var commit setget _set_commit
var repository
var open_file
onready var grid = $Panel/Margin/Rows/Scroll/Grid
onready var text_edit = $Panel/TextEdit
onready var save_button = $Panel/TextEdit/SaveButton
onready var title_label = $Panel/Margin/Rows/Title
func _ready():
update()
_set_mode(mode)
_set_title(title)
func _input(event):
if event.is_action_pressed("save"):
if text_edit.visible:
save()
func clear():
for item in grid.get_children():
item.queue_free()
func update():
if grid:
clear()
match mode:
FileBrowserMode.WORKING_DIRECTORY:
if shell:
var file_string = shell.run("find . -type f")
var files = file_string.split("\n")
files = Array(files)
# The last entry is an empty string, remove it.
files.pop_back()
files.sort_custom(self, "very_best_sort")
for file_path in files:
file_path = file_path.substr(2)
if file_path.substr(0, 5) == ".git/":
continue
var item = preload("res://scenes/file_browser_item.tscn").instance()
item.label = file_path
item.connect("clicked", self, "item_clicked")
grid.add_child(item)
FileBrowserMode.COMMIT:
if commit:
var files = Array(commit.repository.shell.run("git ls-tree --name-only -r %s" % commit.id).split("\n"))
# The last entry is an empty string, remove it.
files.pop_back()
for file_path in files:
var item = preload("res://scenes/file_browser_item.tscn").instance()
item.label = file_path
item.connect("clicked", self, "item_clicked")
grid.add_child(item)
FileBrowserMode.INDEX:
if repository:
var files = Array(repository.shell.run("git ls-files -s | cut -f2").split("\n"))
# The last entry is an empty string, remove it.
files.pop_back()
for file_path in files:
var item = preload("res://scenes/file_browser_item.tscn").instance()
item.label = file_path
item.connect("clicked", self, "item_clicked")
grid.add_child(item)
func item_clicked(item):
print(item.label)
open_file = item.label
match mode:
FileBrowserMode.WORKING_DIRECTORY:
text_edit.text = helpers.read_file(shell._cwd + item.label)
FileBrowserMode.COMMIT:
text_edit.text = commit.repository.shell.run("git show %s:\"%s\"" % [commit.id, item.label])
FileBrowserMode.INDEX:
text_edit.text = repository.shell.run("git show :\"%s\"" % [item.label])
text_edit.show()
text_edit.grab_focus()
func close():
text_edit.hide()
func save():
match mode:
FileBrowserMode.WORKING_DIRECTORY:
var fixme_path = shell._cwd
# Add a newline to the end of the file if there is none.
if text_edit.text.length() > 0 and text_edit.text.substr(text_edit.text.length()-1, 1) != "\n":
text_edit.text += "\n"
helpers.write_file(fixme_path+open_file, text_edit.text)
close()
func _set_commit(new_commit):
commit = new_commit
update()
func _set_mode(new_mode):
mode = new_mode
if save_button:
save_button.visible = mode == FileBrowserMode.WORKING_DIRECTORY
text_edit.readonly = not mode == FileBrowserMode.WORKING_DIRECTORY
text_edit.selecting_enabled = mode == FileBrowserMode.WORKING_DIRECTORY
if mode == FileBrowserMode.WORKING_DIRECTORY:
text_edit.focus_mode = Control.FOCUS_CLICK
else:
text_edit.focus_mode = Control.FOCUS_NONE
func _set_title(new_title):
title = new_title
if title_label:
title_label.text = new_title
func very_best_sort(a,b):
# We're looking at the third character because all entries have the form
# "./.git/bla".
if a.substr(2, 1) == "." and b.substr(2, 1) != ".":
return false
if a.substr(2, 1) != "." and b.substr(2, 1) == ".":
return true
return a.casecmp_to(b) == -1

129
scenes/file_browser.tscn Normal file
View file

@ -0,0 +1,129 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://scenes/file_browser.gd" type="Script" id=1]
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=2]
[ext_resource path="res://styles/theme.tres" type="Theme" id=3]
[node name="FileBrowser" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
rect_min_size = Vector2( 0, 142 )
theme = ExtResource( 3 )
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Panel" type="Panel" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Margin" type="MarginContainer" parent="Panel"]
anchor_right = 1.0
anchor_bottom = 1.0
custom_constants/margin_right = 8
custom_constants/margin_top = 8
custom_constants/margin_left = 8
custom_constants/margin_bottom = 8
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Rows" type="VBoxContainer" parent="Panel/Margin"]
margin_left = 8.0
margin_top = 8.0
margin_right = 1912.0
margin_bottom = 1072.0
custom_constants/separation = 0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Title" type="Label" parent="Panel/Margin/Rows"]
margin_right = 1904.0
margin_bottom = 25.0
text = "title"
align = 1
[node name="Breadcrumbs" type="HBoxContainer" parent="Panel/Margin/Rows"]
visible = false
margin_right = 1856.0
margin_bottom = 50.0
rect_min_size = Vector2( 0, 50 )
custom_constants/separation = 8
[node name="Button" type="Button" parent="Panel/Margin/Rows/Breadcrumbs"]
margin_right = 55.0
margin_bottom = 50.0
text = "root"
[node name="Button2" type="Button" parent="Panel/Margin/Rows/Breadcrumbs"]
margin_left = 63.0
margin_right = 104.0
margin_bottom = 50.0
text = "dir"
[node name="Scroll" type="ScrollContainer" parent="Panel/Margin/Rows"]
margin_top = 25.0
margin_right = 1904.0
margin_bottom = 1064.0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Grid" type="GridContainer" parent="Panel/Margin/Rows/Scroll"]
margin_right = 1904.0
margin_bottom = 1039.0
size_flags_horizontal = 3
size_flags_vertical = 3
custom_constants/vseparation = 16
custom_constants/hseparation = 16
columns = 4
__meta__ = {
"_edit_use_anchors_": false
}
[node name="TextEdit" type="TextEdit" parent="Panel"]
visible = false
anchor_right = 1.0
anchor_bottom = 1.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="SaveButton" type="Button" parent="Panel/TextEdit"]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = -114.396
margin_top = -59.399
margin_right = -14.3955
margin_bottom = -14.399
focus_mode = 0
custom_fonts/font = ExtResource( 2 )
enabled_focus_mode = 0
text = "Save"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="CloseButton" type="Button" parent="Panel/TextEdit"]
anchor_left = 1.0
anchor_right = 1.0
margin_left = -54.3247
margin_top = 12.0
margin_right = -14.3247
margin_bottom = 52.0
focus_mode = 0
custom_fonts/font = ExtResource( 2 )
enabled_focus_mode = 0
text = "x"
__meta__ = {
"_edit_use_anchors_": false
}
[connection signal="pressed" from="Panel/TextEdit/SaveButton" to="." method="save"]
[connection signal="pressed" from="Panel/TextEdit/CloseButton" to="." method="close"]

View file

@ -0,0 +1,19 @@
extends Control
signal clicked(what)
export var label: String setget _set_label
onready var label_node = $VBoxContainer/Label
func _ready():
_set_label(label)
func _set_label(new_label):
label = new_label
if label_node:
label_node.text = helpers.abbreviate(new_label, 30)
func _gui_input(event):
if event is InputEventMouseButton and event.is_pressed() and event.button_index == BUTTON_LEFT:
emit_signal("clicked", self)

View file

@ -0,0 +1,53 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://scenes/file_browser_item.gd" type="Script" id=1]
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=2]
[node name="Control" type="Control"]
anchor_right = 0.052
anchor_bottom = 0.093
margin_right = 100.16
margin_bottom = -0.439995
rect_min_size = Vector2( 150, 100 )
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="VBoxContainer" type="VBoxContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Control" type="Control" parent="VBoxContainer"]
margin_right = 200.0
margin_bottom = 71.0
mouse_filter = 2
size_flags_vertical = 3
[node name="ColorRect" type="ColorRect" parent="VBoxContainer/Control"]
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
margin_left = -25.0
margin_top = -25.0
margin_right = 25.0
margin_bottom = 25.0
mouse_filter = 2
color = Color( 0.203922, 0.721569, 0.501961, 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Label" type="Label" parent="VBoxContainer"]
margin_top = 75.0
margin_right = 200.0
margin_bottom = 100.0
custom_fonts/font = ExtResource( 2 )
text = "filename"
align = 1

67
scenes/game.gd Normal file
View file

@ -0,0 +1,67 @@
extends Node
var tmp_prefix_outside = _tmp_prefix_outside()
var tmp_prefix_inside = _tmp_prefix_inside()
var global_shell
var fake_editor
var dragged_object
var energy = 2
var _file = "user://savegame.json"
var state = {}
func _ready():
var dir = Directory.new()
var repo_dir = tmp_prefix_outside+"repos/"
if not dir.dir_exists(tmp_prefix_outside):
var err = dir.make_dir(tmp_prefix_outside)
if err != OK:
helpers.crash("Could not create temporary directory %s." % tmp_prefix_outside)
if not dir.dir_exists(repo_dir):
var err = dir.make_dir(repo_dir)
if err != OK:
helpers.crash("Could not create temporary directory %s." % repo_dir)
global_shell = Shell.new()
fake_editor = copy_file_to_game_env("fake-editor")
copy_file_to_game_env("fake-editor-noblock")
load_state()
func _initial_state():
return {"history": []}
func save_state():
var savegame = File.new()
savegame.open(_file, File.WRITE)
savegame.store_line(to_json(state))
savegame.close()
func load_state():
var savegame = File.new()
if not savegame.file_exists(_file):
save_state()
savegame.open(_file, File.READ)
state = _initial_state()
var new_state = parse_json(savegame.get_line())
for key in new_state:
state[key] = new_state[key]
savegame.close()
func copy_file_to_game_env(filename):
# Copy fake-editor to tmp directory (because the original might be in a .pck file).
var file_outside = tmp_prefix_outside + filename
var file_inside = tmp_prefix_inside + filename
var content = helpers.read_file("res://scripts/"+filename)
helpers.write_file(file_outside, content)
global_shell.run("chmod u+x " + '"'+file_inside+'"')
return file_inside
func _tmp_prefix_inside():
return OS.get_user_data_dir() + "/tmp/"
func _tmp_prefix_outside():
return "user://tmp/"

134
scenes/helpers.gd Normal file
View file

@ -0,0 +1,134 @@
extends Node
var debug_file_io = false
# Crash the game and display the error message.
func crash(message):
push_error(message)
print("FATAL ERROR: " + message)
get_tree().quit()
# Oh, still here? Let's crash more violently, by calling a non-existing method.
# Violent delights have violent ends.
get_tree().fatal_error()
# Run a simple command with arguments, blocking, using OS.execute.
func exec(command, args=[], crash_on_fail=true):
var debug = false
if debug:
print("exec: %s [%s]" % [command, PoolStringArray(args).join(", ")])
var output = []
var exit_code = OS.execute(command, args, true, output, true)
output = output[0]
if exit_code != 0 and crash_on_fail:
helpers.crash("OS.execute failed: %s [%s] Output: %s \nExit Code %d" % [command, PoolStringArray(args).join(", "), output, exit_code])
elif debug:
print("Output: %s" %output)
return {"output": output, "exit_code": exit_code}
# Return the contents of a file. If no fallback_string is provided, crash when
# the file doesn't exist.
func read_file(path, fallback_string=null):
if debug_file_io:
print("reading " + path)
var file = File.new()
var open_status = file.open(path, File.READ)
if open_status == OK:
var content = file.get_as_text()
file.close()
return content
else:
if fallback_string != null:
return fallback_string
else:
helpers.crash("File %s could not be read, and has no fallback" % path)
func write_file(path, content):
if debug_file_io:
print("writing " + path)
var file = File.new()
file.open(path, File.WRITE)
file.store_string(content)
file.close()
return true
func parse_args():
var arguments = {}
for argument in OS.get_cmdline_args():
if argument.substr(0, 2) == "--":
# Parse valid command-line arguments into a dictionary
if argument.find("=") > -1:
var key_value = argument.split("=")
arguments[key_value[0].lstrip("--")] = key_value[1]
else:
arguments[argument.lstrip("--")] = true
return arguments
func careful_delete(path_inside):
var expected_prefix
var os = OS.get_name()
if os == "X11":
expected_prefix = "/home/%s/.local/share/git-hydra/tmp/" % OS.get_environment("USER")
elif os == "OSX":
expected_prefix = "/Users/%s/Library/Application Support/git-hydra/tmp/" % OS.get_environment("USER")
elif os == "Windows":
expected_prefix = "C:/Users/%s/AppData/Roaming/git-hydra/tmp/" % OS.get_environment("USERNAME")
else:
helpers.crash("Unsupported OS: %s" % os)
if path_inside.substr(0,expected_prefix.length()) != expected_prefix:
helpers.crash("Refusing to delete directory %s that does not start with %s" % [path_inside, expected_prefix])
else:
game.global_shell.cd(game.tmp_prefix_inside)
game.global_shell.run("rm -rf '%s'" % path_inside)
func parse(file):
var text = read_file(file)
var result = {}
var current_section
var section_regex = RegEx.new()
section_regex.compile("^\\[(.*)\\]$")
var assignment_regex = RegEx.new()
assignment_regex.compile("^([a-z ]+)=(.*)$")
for line in text.split("\n"):
# Skip comments.
if line.substr(0, 1) == ";":
continue
# Parse a [section name].
var m = section_regex.search(line)
if m:
current_section = m.get_string(1)
result[current_section] = ""
continue
# Parse a direct=assignment.
m = assignment_regex.search(line)
if m:
var key = m.get_string(1).strip_edges()
var value = m.get_string(2).strip_edges()
result[key] = value
continue
# At this point, the line is just content belonging to the current section.
if current_section:
result[current_section] += line + "\n"
for key in result:
result[key] = result[key].strip_edges()
return result
func abbreviate(text, max_length):
if text.length() > max_length-3:
text = text.substr(0, max_length-3) + "..."
return text

110
scenes/level.gd Normal file
View file

@ -0,0 +1,110 @@
extends Node
class_name Level
var slug
var title
var description
var congrats
var repos = {}
# The path is an outer path.
func load(path):
var parts = path.split("/")
slug = parts[parts.size()-1]
var dir = Directory.new()
if dir.file_exists(path):
# This is a new-style level.
var config = helpers.parse(path)
title = config.get("title", slug)
description = config.get("description", "(no description)")
congrats = config.get("congrats", "Good job, you solved the level!\n\nFeel free to try a few more things or click 'Next Level'.")
var keys = config.keys()
var repo_setups = []
for k in keys:
if k.begins_with("setup"):
repo_setups.push_back(k)
var repo_wins = []
for k in keys:
if k.begins_with("win"):
repo_wins.push_back(k)
for k in repo_setups:
var repo
if " " in k:
repo = Array(k.split(" "))[1]
else:
repo = "yours"
if not repos.has(repo):
repos[repo] = LevelRepo.new()
repos[repo].setup_commands = config[k]
for k in repo_wins:
var repo
if " " in k:
repo = Array(k.split(" "))[1]
else:
repo = "yours"
repos[repo].win_commands = config[k]
elif dir.file_exists(path+"/description"):
# This is an old-style level.
description = helpers.read_file(path+"/description", "(no description)")
congrats = helpers.read_file(path+"/congrats", "Good job, you solved the level!\n\nFeel free to try a few more things or click 'Next Level'.")
var yours = LevelRepo.new()
yours.setup_commands = helpers.read_file(path+"/start", "")
#goal_commands = helpers.read_file(path+"/goal", "")
yours.win_commands = helpers.read_file(path+"/win", "")
repos["yours"] = yours
else:
helpers.crash("Level %s does not exist." % path)
for repo in repos:
repos[repo].path = game.tmp_prefix_inside+"repos/%s/" % repo
repos[repo].slug = repo
# Surround all lines indented with four spaces with [code] tags.
var monospace_regex = RegEx.new()
monospace_regex.compile("\\n ([^\\n]*)")
description = monospace_regex.sub(description, "\n [code]$1[/code]", true)
func construct():
for r in repos:
var repo = repos[r]
# We're actually destroying stuff here.
# Make sure that active_repository is in a temporary directory.
helpers.careful_delete(repo.path)
game.global_shell.run("mkdir '%s'" % repo.path)
game.global_shell.cd(repo.path)
game.global_shell.run("git init")
game.global_shell.run("git symbolic-ref HEAD refs/heads/main")
# Add other repos as remotes.
for r2 in repos:
if r == r2:
continue
game.global_shell.run("git remote add %s %s" % [r2, repos[r2].path])
# Allow receiving a push of the checked-out branch.
game.global_shell.run("git config receive.denyCurrentBranch ignore")
for r in repos:
var repo = repos[r]
game.global_shell.cd(repo.path)
game.global_shell.run(repo.setup_commands)
func check_win():
var won = true
var any_checked = false
for r in repos:
var repo = repos[r]
if repo.win_commands != "":
any_checked = true
game.global_shell.cd(repo.path)
if not game.global_shell.run("function win { %s\n}; win 2>/dev/null >/dev/null && echo yes || echo no" % repo.win_commands) == "yes\n":
won = false
return won and any_checked

7
scenes/level_repo.gd Normal file
View file

@ -0,0 +1,7 @@
extends Node
class_name LevelRepo
var slug
var path
var setup_commands = ""
var win_commands = ""

44
scenes/levels.gd Normal file
View file

@ -0,0 +1,44 @@
extends Node
var chapters
func _ready():
reload()
func reload():
chapters = []
var dir = Directory.new()
dir.open("res://levels")
dir.list_dir_begin()
var chapter_names = []
while true:
var file = dir.get_next()
if file == "":
break
elif not file.begins_with(".") and file != "sequence":
chapter_names.append(file)
dir.list_dir_end()
chapter_names.sort()
var final_chapter_sequence = []
var chapter_sequence = Array(helpers.read_file("res://levels/sequence", "").split("\n"))
for chapter in chapter_sequence:
if chapter == "":
continue
if not chapter_names.has(chapter):
helpers.crash("Chapter '%s' is specified in the sequence, but could not be found" % chapter)
chapter_names.erase(chapter)
final_chapter_sequence.push_back(chapter)
final_chapter_sequence += chapter_names
for c in final_chapter_sequence:
var chapter = Chapter.new()
chapter.load("res://levels/%s" % c)
chapters.push_back(chapter)

136
scenes/main.gd Normal file
View file

@ -0,0 +1,136 @@
extends Control
var dragged = null
var current_chapter
var current_level
onready var terminal = $Rows/Columns/RightSide/Terminal
onready var input = terminal.input
onready var output = terminal.output
onready var repositories_node = $Rows/Columns/Repositories
var repositories = {}
onready var level_select = $Rows/Columns/RightSide/TopStuff/Menu/LevelSelect
onready var chapter_select = $Rows/Columns/RightSide/TopStuff/Menu/ChapterSelect
onready var next_level_button = $Rows/Columns/RightSide/TopStuff/Menu/NextLevelButton
onready var level_name = $Rows/Columns/RightSide/TopStuff/LevelPanel/LevelName
onready var level_description = $Rows/Columns/RightSide/TopStuff/LevelPanel/Text/LevelDescription
onready var level_congrats = $Rows/Columns/RightSide/TopStuff/LevelPanel/Text/LevelCongrats
onready var cards = $Rows/Cards
func _ready():
var args = helpers.parse_args()
if args.has("sandbox"):
var err = get_tree().change_scene("res://scenes/sandbox.tscn")
if err != OK:
helpers.crash("Could not change to sandbox scene")
return
current_chapter = 0
current_level = 0
# Initialize level select.
level_select.connect("item_selected", self, "load_level")
repopulate_levels()
level_select.select(current_level)
# Initialize chapter select.
chapter_select.connect("item_selected", self, "load_chapter")
repopulate_chapters()
chapter_select.select(current_chapter)
# Load first chapter.
load_chapter(current_chapter)
input.grab_focus()
func load_chapter(id):
current_chapter = id
repopulate_levels()
load_level(0)
func load_level(level_id):
next_level_button.hide()
level_congrats.hide()
level_description.show()
current_level = level_id
AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), true)
levels.chapters[current_chapter].levels[current_level].construct()
var level = levels.chapters[current_chapter].levels[current_level]
level_description.bbcode_text = level.description
level_congrats.bbcode_text = level.congrats
level_name.text = level.title
for r in repositories_node.get_children():
r.queue_free()
repositories = {}
var repo_names = level.repos.keys()
repo_names.invert()
for r in repo_names:
var repo = level.repos[r]
var new_repo = preload("res://scenes/repository.tscn").instance()
new_repo.path = repo.path
new_repo.label = repo.slug
new_repo.size_flags_horizontal = SIZE_EXPAND_FILL
repositories_node.add_child(new_repo)
repositories[r] = new_repo
terminal.repository = repositories[repo_names[repo_names.size()-1]]
terminal.clear()
terminal.find_node("TextEditor").close()
# Unmute the audio after a while, so that player can hear pop sounds for
# nodes they create.
var t = Timer.new()
t.wait_time = 1
add_child(t)
t.start()
yield(t, "timeout")
AudioServer.set_bus_mute(AudioServer.get_bus_index("Master"), false)
# FIXME: Need to clean these up when switching levels somehow.
func reload_level():
levels.reload()
load_level(current_level)
func load_next_level():
current_level = (current_level + 1) % levels.chapters[current_chapter].levels.size()
load_level(current_level)
func show_win_status():
if not level_congrats.visible:
next_level_button.show()
level_description.hide()
level_congrats.show()
$SuccessSound.play()
func repopulate_levels():
levels.reload()
level_select.clear()
for level in levels.chapters[current_chapter].levels:
level_select.add_item(level.slug)
level_select.select(current_level)
func repopulate_chapters():
levels.reload()
chapter_select.clear()
for c in levels.chapters:
chapter_select.add_item(c.slug)
chapter_select.select(current_chapter)
func update_repos():
for r in repositories:
var repo = repositories[r]
repo.update_everything()
if levels.chapters[current_chapter].levels[current_level].check_win():
show_win_status()
func toggle_cards():
cards.visible = not cards.visible

212
scenes/main.tscn Normal file
View file

@ -0,0 +1,212 @@
[gd_scene load_steps=9 format=2]
[ext_resource path="res://scenes/terminal.tscn" type="PackedScene" id=1]
[ext_resource path="res://scenes/main.gd" type="Script" id=2]
[ext_resource path="res://scenes/cards.tscn" type="PackedScene" id=3]
[ext_resource path="res://styles/alert_button.tres" type="StyleBox" id=4]
[ext_resource path="res://styles/theme.tres" type="Theme" id=6]
[ext_resource path="res://fonts/big.tres" type="DynamicFont" id=7]
[ext_resource path="res://sounds/success.wav" type="AudioStream" id=8]
[sub_resource type="StyleBoxFlat" id=1]
content_margin_left = 10.0
content_margin_right = 10.0
content_margin_top = 5.0
content_margin_bottom = 5.0
bg_color = Color( 0.847059, 0.0666667, 0.0666667, 1 )
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
[node name="Main" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 8.0
margin_top = 8.0
margin_right = -8.0
margin_bottom = -8.0
mouse_filter = 2
theme = ExtResource( 6 )
script = ExtResource( 2 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="CanvasLayer" type="CanvasLayer" parent="."]
layer = -1
[node name="Background" type="ColorRect" parent="CanvasLayer"]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
color = Color( 0.0705882, 0.0705882, 0.0705882, 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Rows" type="VBoxContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
size_flags_vertical = 3
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Columns" type="HSplitContainer" parent="Rows"]
margin_right = 1904.0
margin_bottom = 784.0
mouse_filter = 2
size_flags_vertical = 3
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Repositories" type="HBoxContainer" parent="Rows/Columns"]
margin_right = 1136.0
margin_bottom = 784.0
mouse_filter = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 1.5
custom_constants/separation = 8
__meta__ = {
"_edit_use_anchors_": false
}
[node name="RightSide" type="VSplitContainer" parent="Rows/Columns"]
margin_left = 1148.0
margin_right = 1904.0
margin_bottom = 784.0
size_flags_horizontal = 3
[node name="TopStuff" type="VBoxContainer" parent="Rows/Columns/RightSide"]
margin_right = 756.0
margin_bottom = 386.0
size_flags_vertical = 3
[node name="Menu" type="HBoxContainer" parent="Rows/Columns/RightSide/TopStuff"]
margin_right = 756.0
margin_bottom = 35.0
[node name="ChapterSelect" type="OptionButton" parent="Rows/Columns/RightSide/TopStuff/Menu"]
margin_right = 168.0
margin_bottom = 35.0
focus_mode = 0
enabled_focus_mode = 0
text = "Select chapter..."
[node name="LevelSelect" type="OptionButton" parent="Rows/Columns/RightSide/TopStuff/Menu"]
margin_left = 173.0
margin_right = 317.0
margin_bottom = 35.0
focus_mode = 0
enabled_focus_mode = 0
text = "Select level..."
expand_icon = true
[node name="ReloadButton" type="Button" parent="Rows/Columns/RightSide/TopStuff/Menu"]
margin_left = 322.0
margin_right = 401.0
margin_bottom = 35.0
focus_mode = 0
enabled_focus_mode = 0
text = "Reload"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="CardsButton" type="Button" parent="Rows/Columns/RightSide/TopStuff/Menu"]
margin_left = 406.0
margin_right = 478.0
margin_bottom = 35.0
focus_mode = 0
enabled_focus_mode = 0
text = "Cards!"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="NextLevelButton" type="Button" parent="Rows/Columns/RightSide/TopStuff/Menu"]
margin_left = 483.0
margin_right = 593.0
margin_bottom = 35.0
focus_mode = 0
custom_styles/hover = SubResource( 1 )
custom_styles/normal = ExtResource( 4 )
enabled_focus_mode = 0
text = "Next Level"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="LevelPanel" type="VBoxContainer" parent="Rows/Columns/RightSide/TopStuff"]
margin_top = 40.0
margin_right = 756.0
margin_bottom = 386.0
size_flags_vertical = 3
[node name="LevelName" type="RichTextLabel" parent="Rows/Columns/RightSide/TopStuff/LevelPanel"]
margin_right = 756.0
margin_bottom = 60.0
rect_min_size = Vector2( 0, 60 )
custom_fonts/normal_font = ExtResource( 7 )
text = "Level name here!"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Text" type="Control" parent="Rows/Columns/RightSide/TopStuff/LevelPanel"]
margin_top = 65.0
margin_right = 756.0
margin_bottom = 346.0
size_flags_vertical = 3
[node name="LevelDescription" type="RichTextLabel" parent="Rows/Columns/RightSide/TopStuff/LevelPanel/Text"]
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_vertical = 3
bbcode_enabled = true
bbcode_text = "Level description here!"
text = "Level description here!"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="LevelCongrats" type="RichTextLabel" parent="Rows/Columns/RightSide/TopStuff/LevelPanel/Text"]
visible = false
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_vertical = 3
bbcode_enabled = true
bbcode_text = "Level description here!"
text = "Level description here!"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Terminal" parent="Rows/Columns/RightSide" instance=ExtResource( 1 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 398.0
margin_right = 756.0
margin_bottom = 784.0
size_flags_vertical = 3
[node name="Cards" parent="Rows" instance=ExtResource( 3 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_top = 789.0
margin_right = 1904.0
margin_bottom = 1064.0
size_flags_vertical = 3
size_flags_stretch_ratio = 0.35
[node name="SuccessSound" type="AudioStreamPlayer" parent="."]
stream = ExtResource( 8 )
[connection signal="button_down" from="Rows/Columns/RightSide/TopStuff/Menu/ChapterSelect" to="." method="repopulate_chapters"]
[connection signal="button_down" from="Rows/Columns/RightSide/TopStuff/Menu/LevelSelect" to="." method="repopulate_levels"]
[connection signal="pressed" from="Rows/Columns/RightSide/TopStuff/Menu/ReloadButton" to="." method="reload_level"]
[connection signal="pressed" from="Rows/Columns/RightSide/TopStuff/Menu/CardsButton" to="." method="toggle_cards"]
[connection signal="pressed" from="Rows/Columns/RightSide/TopStuff/Menu/NextLevelButton" to="." method="load_next_level"]
[connection signal="command_done" from="Rows/Columns/RightSide/Terminal" to="." method="update_repos"]

117
scenes/node.gd Normal file
View file

@ -0,0 +1,117 @@
extends Node2D
var id setget id_set
var content setget content_set
var type setget type_set
var repository: Control
onready var content_label = $Content/ContentLabel
onready var file_browser = $OnTop/FileBrowser
var children = {} setget children_set
var id_always_visible = false
var held = false
var hovered = false
var arrow = preload("res://scenes/arrow.tscn")
func _ready():
content_set(content)
type_set(type)
$Pop.pitch_scale = rand_range(0.8, 1.2)
$Pop.play()
func _process(_delta):
if held:
if not Input.is_action_pressed("click"):
held = false
else:
global_position = get_global_mouse_position()
if visible:
apply_forces()
func apply_forces():
var offset = Vector2(0, 80)
for c in children.keys():
if repository.objects.has(c):
var other = repository.objects[c]
if other.visible:
var d = other.position.distance_to(position+offset)
var dir = (other.position - (position+offset)).normalized()
var f = (d*0.03)
position += dir*f
other.position -= dir*f
func id_set(new_id):
id = new_id
$ID.text = id
func content_set(new_content):
content = new_content
if content_label:
content_label.text = content
func type_set(new_type):
type = new_type
if type == "commit" and file_browser:
file_browser.commit = self
file_browser.title = "Commit"
if type != "ref":
$ID.text = $ID.text.substr(0,8)
match new_type:
"blob":
$Sprite.texture = preload("res://nodes/blob.svg")
"tree":
$Sprite.texture = preload("res://nodes/tree.svg")
"commit":
$Sprite.texture = preload("res://nodes/commit.svg")
"tag":
$Sprite.texture = preload("res://nodes/blob.svg")
"ref":
$Sprite.texture = preload("res://nodes/ref.svg")
id_always_visible = true
"head":
$Sprite.texture = preload("res://nodes/ref.svg")
id_always_visible = true
if id_always_visible:
$ID.show()
func children_set(new_children):
for c in $Arrows.get_children():
if not new_children.has(c.target):
c.queue_free()
for c in new_children:
if not children.has(c):
var a = arrow.instance()
a.source = id
a.target = c
a.repository = repository
$Arrows.add_child(a)
children = new_children
func _on_hover():
hovered = true
if not id_always_visible:
content_label.visible = true
$ID.visible = true
func _on_unhover():
hovered = false
if not id_always_visible:
content_label.visible = false
$ID.visible = false
func _input(event):
if hovered:
if event.is_action_pressed("click"):
held = true
if type == "commit":
file_browser.visible = not file_browser.visible
elif event.is_action_pressed("right_click"):
var input = get_tree().get_current_scene().find_node("Input")
input.text += id
input.caret_position = input.text.length()
if event.is_action_released("click"):
held = false

101
scenes/node.tscn Normal file
View file

@ -0,0 +1,101 @@
[gd_scene load_steps=9 format=2]
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
[ext_resource path="res://scenes/node.gd" type="Script" id=2]
[ext_resource path="res://nodes/blob.svg" type="Texture" id=3]
[ext_resource path="res://scenes/file_browser.tscn" type="PackedScene" id=4]
[ext_resource path="res://nodes/pop.wav" type="AudioStream" id=5]
[ext_resource path="res://scenes/drop_area.tscn" type="PackedScene" id=6]
[sub_resource type="CircleShape2D" id=1]
radius = 23.6295
[sub_resource type="StyleBoxFlat" id=2]
content_margin_left = 5.0
content_margin_right = 5.0
content_margin_top = 5.0
content_margin_bottom = 5.0
bg_color = Color( 0, 0, 0, 0.878431 )
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[node name="Node" type="Node2D"]
script = ExtResource( 2 )
[node name="Arrows" type="Node2D" parent="."]
[node name="Rect" type="ColorRect" parent="."]
visible = false
margin_left = -29.0
margin_top = -28.0
margin_right = 29.0
margin_bottom = 29.0
mouse_filter = 2
color = Color( 1, 1, 1, 0 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Sprite" type="Sprite" parent="."]
scale = Vector2( 0.5, 0.5 )
texture = ExtResource( 3 )
[node name="ID" type="Label" parent="."]
visible = false
margin_left = -19.9265
margin_top = -12.0097
margin_right = 129.073
margin_bottom = 40.9903
custom_fonts/font = ExtResource( 1 )
custom_colors/font_color = Color( 1, 1, 1, 1 )
text = "object_id"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Pop" type="AudioStreamPlayer2D" parent="."]
stream = ExtResource( 5 )
[node name="DropArea" parent="." instance=ExtResource( 6 )]
[node name="Area2D" type="Area2D" parent="."]
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
shape = SubResource( 1 )
[node name="Content" type="Node2D" parent="."]
z_index = 1
[node name="ContentLabel" type="Label" parent="Content"]
visible = false
margin_left = 31.3944
margin_top = -22.8078
margin_right = 41.3944
margin_bottom = 12.1922
custom_styles/normal = SubResource( 2 )
custom_fonts/font = ExtResource( 1 )
custom_colors/font_color = Color( 1, 1, 1, 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="OnTop" type="Node2D" parent="."]
z_index = 2
[node name="FileBrowser" parent="OnTop" instance=ExtResource( 4 )]
visible = false
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = -460.672
margin_top = -23.6409
margin_right = -32.6716
margin_bottom = 118.359
mouse_filter = 1
mode = 1
[connection signal="mouse_entered" from="Rect" to="." method="_on_hover"]
[connection signal="mouse_exited" from="Rect" to="." method="_on_unhover"]
[connection signal="mouse_entered" from="Area2D" to="." method="_on_hover"]
[connection signal="mouse_exited" from="Area2D" to="." method="_on_unhover"]

30
scenes/player.tscn Normal file
View file

@ -0,0 +1,30 @@
[gd_scene load_steps=3 format=2]
[sub_resource type="GDScript" id=1]
script/source = "extends KinematicBody2D
export var speed = 800
func _ready():
pass
func _process(delta):
var right = Input.get_action_strength(\"right\") - Input.get_action_strength(\"left\")
var down = Input.get_action_strength(\"down\") - Input.get_action_strength(\"up\")
move_and_slide(Vector2(right, down).normalized()*speed)
"
[sub_resource type="RectangleShape2D" id=2]
extents = Vector2( 50, 50 )
[node name="Player" type="KinematicBody2D"]
script = SubResource( 1 )
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource( 2 )
[node name="Rect" type="ColorRect" parent="."]
margin_left = -50.0
margin_top = -50.0
margin_right = 50.0
margin_bottom = 50.0

344
scenes/repository.gd Normal file
View file

@ -0,0 +1,344 @@
extends Control
onready var index = $Rows/Browsers/Index
onready var nodes = $Rows/RepoVis/Nodes
onready var file_browser = $Rows/Browsers/FileBrowser
onready var label_node = $Rows/RepoVis/Label
onready var path_node = $Rows/RepoVis/Path
onready var simplify_checkbox = $Rows/RepoVis/SimplifyCheckbox
export var label: String setget set_label
export var path: String setget set_path, get_path
export var file_browser_active = true setget set_file_browser_active
export var simplified_view = false setget set_simplified_view
export var editable_path = false setget set_editable_path
var node = preload("res://scenes/node.tscn")
var shell = Shell.new()
var objects = {}
var mouse_inside = false
# We use this for a heuristic of when to hide trees and blobs.
var _commit_count = 0
func _ready():
file_browser.shell = shell
# Trigger these again because nodes were not ready before.
set_label(label)
set_file_browser_active(file_browser_active)
set_simplified_view(simplified_view)
set_editable_path(editable_path)
set_path(path)
index.repository = self
update_everything()
update_node_positions()
func _process(_delta):
nodes.rect_pivot_offset = nodes.rect_size / 2
if path:
apply_forces()
func _unhandled_input(event):
if event.is_action_pressed("zoom_out") and nodes.rect_scale.x > 0.3:
nodes.rect_scale -= Vector2(0.05, 0.05)
if event.is_action_pressed("zoom_in") and nodes.rect_scale.x < 2:
nodes.rect_scale += Vector2(0.05, 0.05)
func there_is_a_git():
return shell.run("test -d .git && echo yes || echo no") == "yes\n"
func update_everything():
if file_browser:
file_browser.update()
if there_is_a_git():
update_head()
update_refs()
update_index()
update_objects()
remove_gone_stuff()
else:
if index:
index.clear()
for o in objects:
objects[o].queue_free()
objects = {}
func set_path(new_path):
path = new_path
if path_node:
path_node.text = path
if new_path != "":
shell.cd(new_path)
for o in objects.values():
o.queue_free()
objects = {}
if is_inside_tree():
update_everything()
func get_path():
return path
func set_label(new_label):
label = new_label
if label_node:
label_node.text = new_label
func update_index():
index.update()
func random_position():
return Vector2(rand_range(0, rect_size.x), rand_range(0, rect_size.y))
func update_objects():
var all = all_objects()
# Create new objects, if necessary.
for o in all:
if objects.has(o):
continue
var type = object_type(o)
var n = node.instance()
n.id = o
n.type = object_type(o)
n.content = object_content(o)
n.repository = self
match type:
"blob":
pass
"tree":
n.children = tree_children(o)
n.content = n.content.replacen("\t", " ")
"commit":
var c = {}
c[commit_tree(o)] = ""
for p in commit_parents(o):
c[p] = ""
n.children = c
_commit_count += 1
if _commit_count >= 3 and not simplified_view:
set_simplified_view(true)
"tag":
n.children = tag_target(o)
n.position = find_position(n)
nodes.add_child(n)
objects[o] = n
if simplified_view:
if type == "tree" or type == "blob":
n.hide()
func update_node_positions():
if there_is_a_git():
var graph_text = shell.run("git log --graph --oneline --all --no-abbrev")
var graph_lines = Array(graph_text.split("\n"))
graph_lines.pop_back()
for line_count in range(graph_lines.size()):
var line = graph_lines[line_count]
if "*" in line:
var star_idx = line.find("*")
var hash_regex = RegEx.new()
hash_regex.compile("[a-f0-9]+")
var regex_match = hash_regex.search(line)
objects[regex_match.get_string()].position = Vector2(star_idx * 100 + 500, line_count * 100 + 500)
for ref in all_refs():
var target_reference = objects[ref].children.keys()[0]
var target = objects[target_reference]
objects[ref].position = Vector2(target.position.x ,target.position.y - 100)
var target_reference = objects["HEAD"].children.keys()[0]
if objects.has(target_reference):
var target = objects[target_reference]
objects["HEAD"].position = Vector2(target.position.x ,target.position.y - 100)
func update_refs():
for r in all_refs():
if not objects.has(r):
var n = node.instance()
n.id = r
n.type = "ref"
n.content = ""
n.repository = self
objects[r] = n
n.children = {ref_target(r): ""}
n.position = find_position(n)
nodes.add_child(n)
var n = objects[r]
n.children = {ref_target(r): ""}
func apply_forces():
for o in objects.values():
if not o.visible:
continue
for o2 in objects.values():
if o == o2 or not o2.visible:
continue
var d = o.position.distance_to(o2.position)
var dir = (o.global_position - o2.global_position).normalized()
var f = 2000/pow(d+0.00001,1.5)
o.position += dir*f
o2.position -= dir*f
var center_of_gravity = nodes.rect_size/2
var d = o.position.distance_to(center_of_gravity)
var dir = (o.position - center_of_gravity).normalized()
var f = (d+0.00001)*(Vector2(nodes.rect_size.y, nodes.rect_size.x/3).normalized()/30)
o.position -= dir*f
func find_position(n):
var position = Vector2.ZERO
var count = 0
for child in n.children:
if objects.has(child):
position += objects[child].position
count += 1
if count > 0:
position /= count
n.position = position + Vector2(0, -150)
else:
n.position = random_position()
return n.position
func git(args, splitlines = false):
var o = shell.run("git --no-replace-objects " + args)
if splitlines:
o = o.split("\n")
# Remove last empty line.
o.remove(len(o)-1)
else:
# Remove trailing newline.
o = o.substr(0,len(o)-1)
return o
func update_head():
if not objects.has("HEAD"):
var n = node.instance()
n.id = "HEAD"
n.type = "head"
n.content = ""
n.repository = self
n.position = find_position(n)
objects["HEAD"] = n
nodes.add_child(n)
var n = objects["HEAD"]
n.children = {ref_target("HEAD"): ""}
func all_objects():
var obj = git("cat-file --batch-check='%(objectname)' --batch-all-objects", true)
var dict = {}
for o in obj:
dict[o] = ""
return dict
func object_type(id):
return git("cat-file -t "+id)
func object_content(id):
return git("cat-file -p "+id)
func tree_children(id):
var children = git("cat-file -p "+id, true)
var ids = {}
for c in children:
var a = c.split(" ")
ids[a[2].split("\t")[0]] = a[2].split("\t")[1]
return ids
func commit_tree(id):
var c = git("cat-file -p "+id, true)
for cc in c:
var ccc = cc.split(" ", 2)
match ccc[0]:
"tree":
return ccc[1]
return null
func commit_parents(id):
var parents = []
var c = git("cat-file -p "+id, true)
for cc in c:
var ccc = cc.split(" ", 2)
match ccc[0]:
"parent":
parents.push_back(ccc[1])
return parents
func tag_target(id):
var c = git("rev-parse %s^{}" % id)
return {c: ""}
func all_refs():
var refs = {}
# If there are no refs, show-ref will have exit code 1. We don't care.
for line in git("show-ref || true", true):
line = line.split(" ")
var _id = line[0]
var name = line[1]
refs[name] = ""
return refs
func ref_target(ref):
# Test whether this is a symbolic ref.
var ret = git("symbolic-ref -q "+ref+" || true")
# If it's not, it's probably a regular ref.
if ret == "":
if ref == "HEAD":
ret = git("show-ref --head "+ref).split(" ")[0]
else:
ret = git("show-ref "+ref).split(" ")[0]
return ret
func set_simplified_view(simplify):
simplified_view = simplify
if simplify_checkbox:
simplify_checkbox.pressed = simplify
for o in objects:
var obj = objects[o]
if obj.type == "tree" or obj.type == "blob":
obj.visible = not simplify
func set_editable_path(editable):
editable_path = editable
if label_node:
label_node.visible = not editable
if path_node:
path_node.visible = editable
func remove_gone_stuff():
# FIXME: Cache the result of all_objects.
var all = {}
for o in all_objects():
all[o] = ""
for o in all_refs():
all[o] = ""
all["HEAD"] = ""
# Delete objects, if they disappeared.
for o in objects.keys():
if not all.has(o):
objects[o].queue_free()
objects.erase(o)
func _on_mouse_entered():
mouse_inside = true
func _on_mouse_exited():
mouse_inside = false
func set_file_browser_active(active):
file_browser_active = active
if file_browser:
file_browser.visible = active

128
scenes/repository.tscn Normal file
View file

@ -0,0 +1,128 @@
[gd_scene load_steps=5 format=2]
[ext_resource path="res://scenes/repository.gd" type="Script" id=1]
[ext_resource path="res://styles/theme.tres" type="Theme" id=2]
[ext_resource path="res://fonts/big.tres" type="DynamicFont" id=3]
[ext_resource path="res://scenes/file_browser.tscn" type="PackedScene" id=4]
[node name="Repository" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
theme = ExtResource( 2 )
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Rows" type="VSplitContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
__meta__ = {
"_edit_use_anchors_": false
}
[node name="RepoVis" type="Control" parent="Rows"]
margin_right = 1920.0
margin_bottom = 926.0
mouse_filter = 2
size_flags_vertical = 3
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Label" type="Label" parent="Rows/RepoVis"]
margin_left = 5.60091
margin_top = -0.518692
margin_right = 204.601
margin_bottom = 48.4813
custom_fonts/font = ExtResource( 3 )
text = "Repo name"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="IndexLabel" type="Label" parent="Rows/RepoVis"]
visible = false
margin_left = 21.0
margin_top = 65.0
margin_right = 377.0
margin_bottom = 108.0
text = "Index:"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Button" type="Button" parent="Rows/RepoVis"]
visible = false
margin_left = 36.5602
margin_top = 67.9891
margin_right = 119.56
margin_bottom = 109.989
text = "Update"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="SimplifyCheckbox" type="CheckBox" parent="Rows/RepoVis"]
visible = false
anchor_left = 1.0
anchor_right = 1.0
margin_left = -208.715
margin_top = 17.9594
margin_right = -15.7146
margin_bottom = 42.9594
focus_mode = 0
enabled_focus_mode = 0
text = "Hide trees and blobs"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Nodes" type="Control" parent="Rows/RepoVis"]
anchor_right = 1.0
anchor_bottom = 1.0
margin_bottom = -6.10352e-05
mouse_filter = 2
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Path" type="LineEdit" parent="Rows/RepoVis"]
visible = false
margin_left = 23.0
margin_top = 12.0
margin_right = 374.0
margin_bottom = 61.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Browsers" type="VBoxContainer" parent="Rows"]
margin_top = 938.0
margin_right = 1920.0
margin_bottom = 1080.0
[node name="Index" parent="Rows/Browsers" instance=ExtResource( 4 )]
visible = false
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 1920.0
margin_bottom = 142.0
size_flags_vertical = 3
title = "Index"
mode = 2
[node name="FileBrowser" parent="Rows/Browsers" instance=ExtResource( 4 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 1920.0
margin_bottom = 142.0
size_flags_vertical = 3
title = "Working directory"
[connection signal="mouse_entered" from="." to="." method="_on_mouse_entered"]
[connection signal="mouse_exited" from="." to="." method="_on_mouse_exited"]
[connection signal="pressed" from="Rows/RepoVis/Button" to="." method="update_everything"]
[connection signal="toggled" from="Rows/RepoVis/SimplifyCheckbox" to="." method="set_simplified_view"]
[connection signal="text_entered" from="Rows/RepoVis/Path" to="." method="set_path"]

33
scenes/sandbox.gd Normal file
View file

@ -0,0 +1,33 @@
extends Control
func _ready():
var path = null
var args = helpers.parse_args()
if args.has("sandbox"):
if args["sandbox"] is String:
if args["sandbox"] == ".":
args["sandbox"] = OS.get_environment("PWD")
var dir = Directory.new()
if dir.dir_exists(args["sandbox"]):
path = args["sandbox"]
else:
helpers.crash("Directory %s does not exist" % args["sandbox"])
if path == null:
path = game.tmp_prefix_inside+"/repos/sandbox/"
helpers.careful_delete(path)
game.global_shell.run("mkdir " + path)
game.global_shell.cd(path)
game.global_shell.run("git init")
game.global_shell.run("git symbolic-ref HEAD refs/heads/main")
$Columns/Repository.path = path
get_tree().set_screen_stretch(SceneTree.STRETCH_MODE_2D, SceneTree.STRETCH_ASPECT_KEEP, Vector2(1920, 1080), 1.5)
$Columns/Terminal.repository = $Columns/Repository
func update_repo():
$Columns/Repository.update_everything()

53
scenes/sandbox.tscn Normal file
View file

@ -0,0 +1,53 @@
[gd_scene load_steps=5 format=2]
[ext_resource path="res://scenes/terminal.tscn" type="PackedScene" id=1]
[ext_resource path="res://scenes/repository.tscn" type="PackedScene" id=2]
[ext_resource path="res://styles/theme.tres" type="Theme" id=3]
[ext_resource path="res://sandbox.gd" type="Script" id=4]
[node name="Sandbox" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
theme = ExtResource( 3 )
script = ExtResource( 4 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Background" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
color = Color( 0.0705882, 0.0705882, 0.0705882, 1 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Columns" type="HSplitContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 5.0
margin_top = 5.0
margin_right = -5.0
margin_bottom = -5.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Repository" parent="Columns" instance=ExtResource( 2 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_right = 949.0
margin_bottom = 1070.0
size_flags_horizontal = 3
editable_path = true
[node name="Terminal" parent="Columns" instance=ExtResource( 1 )]
anchor_right = 0.0
anchor_bottom = 0.0
margin_left = 961.0
margin_right = 1910.0
margin_bottom = 1070.0
size_flags_horizontal = 3
[connection signal="command_done" from="Columns/Terminal" to="." method="update_repo"]

110
scenes/shell.gd Normal file
View file

@ -0,0 +1,110 @@
extends Node
class_name Shell
var exit_code
var _cwd
var _os = OS.get_name()
func _init():
_cwd = game.tmp_prefix_inside
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 debug = false
if debug:
print("$ %s" % command)
var env = {}
if game.fake_editor:
env["GIT_EDITOR"] = game.fake_editor.replace(" ", "\\ ")
env["GIT_AUTHOR_NAME"] = "You"
env["GIT_COMMITTER_NAME"] = "You"
env["GIT_AUTHOR_EMAIL"] = "you@example.com"
env["GIT_COMMITTER_EMAIL"] = "you@example.com"
env["GIT_TEMPLATE_DIR"] = ""
var hacky_command = ""
for variable in env:
hacky_command += "export %s='%s';" % [variable, env[variable]]
hacky_command += "cd '%s' || exit 1;" % _cwd
hacky_command += command
var result
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_inside + "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 debug:
print(result["output"])
exit_code = result["exit_code"]
return result["output"]
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)
var _t
func run_async(command):
_t = Thread.new()
_t.start(self, "run_async_thread", command)
func run_async_thread(command):
var port = 1000 + (randi() % 1000)
var s = TCP_Server.new()
s.listen(port)
var _pid = OS.execute("ncat", ["127.0.0.1", str(port), "-c", command], false, [], true)
while not s.is_connection_available():
pass
var c = s.take_connection()
while c.get_status() == StreamPeerTCP.STATUS_CONNECTED:
read_from(c)
OS.delay_msec(1000/30)
read_from(c)
c.disconnect_from_host()
s.stop()
func read_from(c):
var total_available = c.get_available_bytes()
print(str(total_available)+" bytes available")
while total_available > 0:
var available = min(1024, total_available)
total_available -= available
print("reading "+str(available))
var data = c.get_utf8_string(available)
#emit_signal("output", data)
print(data.size())

41
scenes/tcp_server.gd Normal file
View file

@ -0,0 +1,41 @@
extends Node
signal data_received(string)
export var port: int
var _s = TCP_Server.new()
var _c
var _connected = false
func _ready():
start()
func start():
_s.listen(port)
func _process(_delta):
if _s.is_connection_available():
if _connected:
_c.disconnect_from_host()
helpers.crash("Dropping active connection")
_c = _s.take_connection()
_connected = true
print("connected!")
if _connected:
if _c.get_status() != StreamPeerTCP.STATUS_CONNECTED:
_connected = false
print("disconnected")
var available = _c.get_available_bytes()
while available > 0:
var data = _c.get_utf8_string(available)
emit_signal("data_received", data)
available = _c.get_available_bytes()
func send(text):
if _connected:
text += "\n"
_c.put_data(text.to_utf8())
else:
helpers.crash("Trying to send data on closed connection")

7
scenes/tcp_server.tscn Normal file
View file

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scenes/tcp_server.gd" type="Script" id=1]
[node name="TCPServer" type="Node"]
script = ExtResource( 1 )
port = 6666

212
scenes/terminal.gd Normal file
View file

@ -0,0 +1,212 @@
extends Control
signal command_done
var thread
var history_position = 0
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"]
var git_commands_help = []
onready var input = $Rows/InputLine/Input
onready var output = $Rows/TopHalf/Output
onready var completions = $Rows/TopHalf/Completions
var repository
onready var main = get_tree().get_root().get_node("Main")
var premade_commands = [
'git commit --allow-empty -m "empty"',
'echo $RANDOM | git hash-object -w --stdin',
'git switch -c $RANDOM',
]
func _ready():
var error = $TextEditor.connect("hide", self, "editor_closed")
if error != OK:
helpers.crash("Could not connect TextEditor's hide signal")
input.grab_focus()
for subcommand in git_commands:
git_commands_help.push_back("")
completions.hide()
history_position = game.state["history"].size()
func _input(event):
if game.state["history"].size() > 0:
if event.is_action_pressed("ui_up"):
if history_position > 0:
history_position -= 1
input.text = game.state["history"][history_position]
input.caret_position = input.text.length()
# This prevents the Input taking the arrow as a "skip to beginning" command.
get_tree().set_input_as_handled()
if event.is_action_pressed("ui_down"):
if history_position < game.state["history"].size()-1:
history_position += 1
input.text = game.state["history"][history_position]
input.caret_position = input.text.length()
get_tree().set_input_as_handled()
if event.is_action_pressed("tab_complete"):
if completions.visible:
completions.get_root().get_children().select(0)
get_tree().set_input_as_handled()
if event.is_action_pressed("delete_word"):
var first_half = input.text.substr(0,input.caret_position)
var second_half = input.text.substr(input.caret_position)
var idx = first_half.strip_edges(false, true).find_last(" ")
if idx > 0:
input.text = first_half.substr(0,idx+1) + second_half
input.caret_position = idx+1
else:
input.text = "" + second_half
func load_command(id):
input.text = premade_commands[id]
input.caret_position = input.text.length()
func send_command(command):
game.state["history"].push_back(command)
game.save_state()
history_position = game.state["history"].size()
input.editable = false
completions.hide()
if thread != null:
thread.wait_to_finish()
thread = Thread.new()
thread.start(self, "run_command_in_a_thread", command)
func send_command_async(command):
input.text = ""
$TCPServer.send(command+"\n")
func run_command_in_a_thread(command):
var o = repository.shell.run(command, false)
if repository.shell.exit_code == 0:
$OkSound.pitch_scale = rand_range(0.8, 1.2)
$OkSound.play()
else:
$ErrorSound.play()
input.text = ""
input.editable = true
if o.length() <= 1000:
output.text = output.text + "$ " + command + "\n" + o
else:
$Pager/Text.text = o
$Pager.popup()
emit_signal("command_done")
func receive_output(text):
output.text += text
repository.update_everything()
func clear():
output.text = ""
func editor_closed():
input.grab_focus()
func regenerate_completions_menu(new_text):
var comp = generate_completions(new_text)
completions.clear()
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)
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])
completions.margin_top = -min(filtered_comp.size() * 35 + 10, 210)
func relevant_subcommands():
var result = {}
for h in game.state["history"]:
var parts = Array(h.split(" "))
if parts[0] == "git":
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]])
result_array.sort_custom(self, "sort_by_frequency_desc")
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]
func generate_completions(command):
var results = []
# Collect git commands.
if command.substr(0, 4) == "git ":
var rest = command.substr(4)
var subcommands = relevant_subcommands()
for sc in subcommands:
if sc.substr(0, rest.length()) == rest:
results.push_back("git "+sc)
# 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()
var file_string = repository.shell.run("find . -type f")
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
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()
input.caret_position = input.text.length()
func editor_saved():
emit_signal("command_done")

173
scenes/terminal.tscn Normal file
View file

@ -0,0 +1,173 @@
[gd_scene load_steps=10 format=2]
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
[ext_resource path="res://sounds/typewriter_ding.wav" type="AudioStream" id=2]
[ext_resource path="res://fonts/monospace.tres" type="DynamicFont" id=3]
[ext_resource path="res://scenes/terminal.gd" type="Script" id=4]
[ext_resource path="res://scenes/text_editor.tscn" type="PackedScene" id=5]
[ext_resource path="res://scenes/tcp_server.tscn" type="PackedScene" id=6]
[ext_resource path="res://sounds/buzzer.wav" type="AudioStream" id=7]
[sub_resource type="StyleBoxFlat" id=1]
content_margin_left = 5.0
content_margin_right = 5.0
content_margin_top = 5.0
content_margin_bottom = 5.0
bg_color = Color( 0, 0, 0, 1 )
border_color = Color( 0.415686, 0.333333, 1, 1 )
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10
[sub_resource type="GDScript" id=2]
script/source = "extends Button
func _ready():
pass
func pressed():
$\"../../..\".send_command(text)
"
[node name="Terminal" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 1
script = ExtResource( 4 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Rows" type="VBoxContainer" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="TopHalf" type="Control" parent="Rows"]
margin_right = 1920.0
margin_bottom = 1052.0
size_flags_vertical = 3
[node name="Output" type="RichTextLabel" parent="Rows/TopHalf"]
anchor_right = 1.0
anchor_bottom = 1.0
margin_top = -1.92206
margin_bottom = -1.92212
size_flags_vertical = 3
custom_styles/normal = SubResource( 1 )
custom_fonts/normal_font = ExtResource( 3 )
scroll_following = true
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Completions" type="Tree" parent="Rows/TopHalf"]
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_top = -311.0
columns = 2
hide_root = true
__meta__ = {
"_edit_use_anchors_": false
}
[node name="VBoxContainer" type="VBoxContainer" parent="Rows"]
visible = false
margin_top = 984.0
margin_right = 1920.0
margin_bottom = 1052.0
[node name="Button" type="Button" parent="Rows/VBoxContainer"]
margin_right = 1920.0
margin_bottom = 20.0
text = "git commit --allow-empty -m \"$RANDOM\""
script = SubResource( 2 )
[node name="Button2" type="Button" parent="Rows/VBoxContainer"]
margin_top = 24.0
margin_right = 1920.0
margin_bottom = 44.0
text = "git checkout HEAD^"
script = SubResource( 2 )
[node name="Button3" type="Button" parent="Rows/VBoxContainer"]
margin_top = 48.0
margin_right = 1920.0
margin_bottom = 68.0
text = "git checkout -b \"$RANDOM\""
script = SubResource( 2 )
[node name="InputLine" type="HBoxContainer" parent="Rows"]
margin_top = 1056.0
margin_right = 1920.0
margin_bottom = 1080.0
[node name="Input" type="LineEdit" parent="Rows/InputLine"]
margin_right = 1920.0
margin_bottom = 24.0
size_flags_horizontal = 3
caret_blink = true
__meta__ = {
"_edit_use_anchors_": false
}
[node name="ClearButton" type="Button" parent="."]
anchor_left = 1.0
anchor_right = 1.0
margin_left = -88.0
margin_top = 5.0
margin_right = -5.0
margin_bottom = 36.0
focus_mode = 0
custom_fonts/font = ExtResource( 1 )
enabled_focus_mode = 0
text = "Clear"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="TextEditor" parent="." instance=ExtResource( 5 )]
visible = false
mouse_filter = 1
syntax_highlighting = false
[node name="TCPServer" parent="." instance=ExtResource( 6 )]
[node name="Pager" type="WindowDialog" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = 18.0
margin_top = 39.0
margin_right = -687.0
margin_bottom = -48.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="Text" type="RichTextLabel" parent="Pager"]
anchor_right = 1.0
anchor_bottom = 1.0
custom_fonts/normal_font = ExtResource( 3 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="ErrorSound" type="AudioStreamPlayer" parent="."]
stream = ExtResource( 7 )
[node name="OkSound" type="AudioStreamPlayer" parent="."]
stream = ExtResource( 2 )
[connection signal="item_selected" from="Rows/TopHalf/Completions" to="." method="_completion_selected"]
[connection signal="pressed" from="Rows/VBoxContainer/Button" to="Rows/VBoxContainer/Button" method="pressed"]
[connection signal="pressed" from="Rows/VBoxContainer/Button2" to="Rows/VBoxContainer/Button2" method="pressed"]
[connection signal="pressed" from="Rows/VBoxContainer/Button3" to="Rows/VBoxContainer/Button3" method="pressed"]
[connection signal="text_changed" from="Rows/InputLine/Input" to="." method="_input_changed"]
[connection signal="text_entered" from="Rows/InputLine/Input" to="." method="send_command"]
[connection signal="pressed" from="ClearButton" to="." method="clear"]
[connection signal="saved" from="TextEditor" to="." method="editor_saved"]
[connection signal="data_received" from="TCPServer" to="." method="receive_output"]

53
scenes/text_editor.gd Normal file
View file

@ -0,0 +1,53 @@
extends TextEdit
signal saved
var path
var _server
var _client_connection
func _ready():
# Initialize TCP server for fake editor.
_server = TCP_Server.new()
_server.listen(1234)
func _process(_delta):
if _server.is_connection_available():
_client_connection = _server.take_connection()
var length = _client_connection.get_u8()
var filename = _client_connection.get_string(length)
filename = filename.replace("%srepos/" % game.tmp_prefix_inside, "")
open(filename)
func _input(event):
if event.is_action_pressed("save"):
save()
func open(filename):
path = filename
var fixme_path = game.tmp_prefix_outside+"repos/"
var content = helpers.read_file(fixme_path+filename)
text = content
show()
grab_focus()
func save():
if visible:
var fixme_path = game.tmp_prefix_outside+"repos/"
# Add a newline to the end of the file if there is none.
if text.length() > 0 and text.substr(text.length()-1, 1) != "\n":
text += "\n"
helpers.write_file(fixme_path+path, text)
close()
emit_signal("saved")
func close():
if _client_connection and _client_connection.is_connected_to_host():
_client_connection.disconnect_from_host()
text = ""
hide()

50
scenes/text_editor.tscn Normal file
View file

@ -0,0 +1,50 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://fonts/default.tres" type="DynamicFont" id=1]
[ext_resource path="res://scenes/text_editor.gd" type="Script" id=2]
[node name="TextEditor" type="TextEdit"]
anchor_right = 1.0
anchor_bottom = 1.0
custom_colors/background_color = Color( 0, 0, 0, 1 )
text = "Text here"
syntax_highlighting = true
script = ExtResource( 2 )
__meta__ = {
"_edit_use_anchors_": false
}
[node name="SaveButton" type="Button" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_left = -114.396
margin_top = -59.399
margin_right = -14.3955
margin_bottom = -14.399
focus_mode = 0
custom_fonts/font = ExtResource( 1 )
enabled_focus_mode = 0
text = "Save"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="CloseButton" type="Button" parent="."]
anchor_left = 1.0
anchor_right = 1.0
margin_left = -54.3247
margin_top = 12.0
margin_right = -14.3247
margin_bottom = 52.0
focus_mode = 0
custom_fonts/font = ExtResource( 1 )
enabled_focus_mode = 0
text = "x"
__meta__ = {
"_edit_use_anchors_": false
}
[connection signal="pressed" from="SaveButton" to="." method="save"]
[connection signal="pressed" from="CloseButton" to="." method="close"]