summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Vella <nick@nxk.io>2021-02-13 21:22:48 +1100
committerAndreas Kling <kling@serenityos.org>2021-02-13 19:49:30 +0100
commitb6715772234e3e55235b21fff05487e29727bfe8 (patch)
tree39a93db4d872f1de1c04985b11f78009fc02d3cd
parenta6fdc17f3f0add00fd15a1223415e8dd04ceb6f8 (diff)
downloadserenity-b6715772234e3e55235b21fff05487e29727bfe8.zip
HackStudio: Project templates and New Project dialog
This commit adds a simple project template system to HackStudio, as well as a pretty New Project dialog, inspired by early VS.NET and MS Office.
-rw-r--r--Base/res/devel/templates/cpp-basic.ini5
-rw-r--r--Base/res/devel/templates/cpp-basic.postcreate19
-rw-r--r--Base/res/devel/templates/cpp-basic/main.cpp7
-rw-r--r--Base/res/devel/templates/cpp-gui.ini5
-rw-r--r--Base/res/devel/templates/cpp-gui.postcreate19
-rw-r--r--Base/res/devel/templates/cpp-gui/main.cpp26
-rw-r--r--Base/res/devel/templates/cpp-library.ini4
-rw-r--r--Base/res/devel/templates/cpp-library.postcreate49
-rw-r--r--Base/res/devel/templates/empty.ini5
-rw-r--r--Base/res/icons/hackstudio/templates-32x32/cpp-basic.pngbin0 -> 511 bytes
-rw-r--r--Base/res/icons/hackstudio/templates-32x32/cpp-gui.pngbin0 -> 2559 bytes
-rw-r--r--Base/res/icons/hackstudio/templates-32x32/cpp-library.pngbin0 -> 2933 bytes
-rw-r--r--Base/res/icons/hackstudio/templates-32x32/empty.pngbin0 -> 6008 bytes
-rwxr-xr-xMeta/build-root-filesystem.sh1
-rw-r--r--Userland/DevTools/HackStudio/CMakeLists.txt8
-rw-r--r--Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.cpp244
-rw-r--r--Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.gml115
-rw-r--r--Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.h79
-rw-r--r--Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp156
-rw-r--r--Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.h73
-rw-r--r--Userland/DevTools/HackStudio/HackStudioWidget.cpp16
-rw-r--r--Userland/DevTools/HackStudio/HackStudioWidget.h2
-rw-r--r--Userland/DevTools/HackStudio/ProjectTemplate.cpp279
-rw-r--r--Userland/DevTools/HackStudio/ProjectTemplate.h67
24 files changed, 1178 insertions, 1 deletions
diff --git a/Base/res/devel/templates/cpp-basic.ini b/Base/res/devel/templates/cpp-basic.ini
new file mode 100644
index 0000000000..da86eb8f05
--- /dev/null
+++ b/Base/res/devel/templates/cpp-basic.ini
@@ -0,0 +1,5 @@
+[HackStudioTemplate]
+Name=Command-line Application (C++)
+Description=Template for creating a basic C++ command-line application.
+Priority=95
+IconName32x=cpp-basic
diff --git a/Base/res/devel/templates/cpp-basic.postcreate b/Base/res/devel/templates/cpp-basic.postcreate
new file mode 100644
index 0000000000..497471f452
--- /dev/null
+++ b/Base/res/devel/templates/cpp-basic.postcreate
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+echo "PROGRAM = $1" >> $2/Makefile
+echo "OBJS = main.o" >> $2/Makefile
+echo "CXXFLAGS = -g -std=c++2a" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "all: \$(PROGRAM)" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "\$(PROGRAM): \$(OBJS)" >> $2/Makefile
+echo " \$(CXX) -o \$@ \$(OBJS)" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "%.o: %.cpp" >> $2/Makefile
+echo " \$(CXX) \$(CXXFLAGS) -o \$@ -c \$< " >> $2/Makefile
+echo "" >> $2/Makefile
+echo "clean:" >> $2/Makefile
+echo " rm \$(OBJS) \$(PROGRAM)" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "run:" >> $2/Makefile
+echo " ./\$(PROGRAM)" >> $2/Makefile
diff --git a/Base/res/devel/templates/cpp-basic/main.cpp b/Base/res/devel/templates/cpp-basic/main.cpp
new file mode 100644
index 0000000000..c6644f3a60
--- /dev/null
+++ b/Base/res/devel/templates/cpp-basic/main.cpp
@@ -0,0 +1,7 @@
+#include <stdio.h>
+
+int main(int argc, char** argv)
+{
+ printf("Hello friends!\n");
+ return 0;
+}
diff --git a/Base/res/devel/templates/cpp-gui.ini b/Base/res/devel/templates/cpp-gui.ini
new file mode 100644
index 0000000000..4af451fff0
--- /dev/null
+++ b/Base/res/devel/templates/cpp-gui.ini
@@ -0,0 +1,5 @@
+[HackStudioTemplate]
+Name=Graphical Application (C++)
+Description=Template for creating a basic C++ graphical application.
+Priority=90
+IconName32x=cpp-gui
diff --git a/Base/res/devel/templates/cpp-gui.postcreate b/Base/res/devel/templates/cpp-gui.postcreate
new file mode 100644
index 0000000000..822de7f1f7
--- /dev/null
+++ b/Base/res/devel/templates/cpp-gui.postcreate
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+echo "PROGRAM = $1" >> $2/Makefile
+echo "OBJS = main.o" >> $2/Makefile
+echo "CXXFLAGS = -lgui -g -std=c++2a" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "all: \$(PROGRAM)" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "\$(PROGRAM): \$(OBJS)" >> $2/Makefile
+echo " \$(CXX) \$(CXXFLAGS) -o \$@ \$(OBJS)" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "%.o: %.cpp" >> $2/Makefile
+echo " \$(CXX) \$(CXXFLAGS) -o \$@ -c \$< " >> $2/Makefile
+echo "" >> $2/Makefile
+echo "clean:" >> $2/Makefile
+echo " rm \$(OBJS) \$(PROGRAM)" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "run:" >> $2/Makefile
+echo " ./\$(PROGRAM)" >> $2/Makefile
diff --git a/Base/res/devel/templates/cpp-gui/main.cpp b/Base/res/devel/templates/cpp-gui/main.cpp
new file mode 100644
index 0000000000..35629bc8d3
--- /dev/null
+++ b/Base/res/devel/templates/cpp-gui/main.cpp
@@ -0,0 +1,26 @@
+#include <stdio.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/Window.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/MessageBox.h>
+
+int main(int argc, char** argv)
+{
+ auto app = GUI::Application::construct(argc, argv);
+
+ auto window = GUI::Window::construct();
+ window->set_title("Hello friends!");
+ window->resize(200, 100);
+
+ auto button = GUI::Button::construct();
+ button->set_text("Click me!");
+ button->on_click = [&](auto) {
+ GUI::MessageBox::show(window, "Hello friends!", ":^)");
+ };
+
+ window->set_main_widget(button);
+
+ window->show();
+
+ return app->exec();
+}
diff --git a/Base/res/devel/templates/cpp-library.ini b/Base/res/devel/templates/cpp-library.ini
new file mode 100644
index 0000000000..fd657bb6d7
--- /dev/null
+++ b/Base/res/devel/templates/cpp-library.ini
@@ -0,0 +1,4 @@
+[HackStudioTemplate]
+Name=Shared Library (C++)
+Description=Template for creating a C++ shared library.
+IconName32x=cpp-library
diff --git a/Base/res/devel/templates/cpp-library.postcreate b/Base/res/devel/templates/cpp-library.postcreate
new file mode 100644
index 0000000000..3f4a254ebb
--- /dev/null
+++ b/Base/res/devel/templates/cpp-library.postcreate
@@ -0,0 +1,49 @@
+#!/bin/sh
+
+# $1: Project name, filesystem safe
+# $2: Project full path
+# $3: Project name, namespace safe
+
+# Generate Makefile
+echo "LIBRARY = $1.so" >> $2/Makefile
+echo "OBJS = Class1.o" >> $2/Makefile
+echo "CXXFLAGS = -g -std=c++2a" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "all: \$(LIBRARY)" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "\$(LIBRARY): \$(OBJS)" >> $2/Makefile
+echo " \$(CXX) -shared -o \$@ \$(OBJS)" >> $2/Makefile
+echo "" >> $2/Makefile
+echo "%.o: %.cpp" >> $2/Makefile
+echo " \$(CXX) \$(CXXFLAGS) -fPIC -o \$@ -c \$< " >> $2/Makefile
+echo "" >> $2/Makefile
+echo "clean:" >> $2/Makefile
+echo " rm \$(OBJS) \$(LIBRARY)" >> $2/Makefile
+echo "" >> $2/Makefile
+
+# Generate 'Class1' header file
+echo "#pragma once" >> $2/Class1.h
+echo "" >> $2/Class1.h
+echo "namespace $3 {" >> $2/Class1.h
+echo "" >> $2/Class1.h
+echo "class Class1 {" >> $2/Class1.h
+echo "public:" >> $2/Class1.h
+echo " void hello();" >> $2/Class1.h
+echo "};" >> $2/Class1.h
+echo "" >> $2/Class1.h
+echo "}" >> $2/Class1.h
+echo "" >> $2/Class1.h
+
+# Generate 'Class1' source file
+echo "#include \"Class1.h\"" >> $2/Class1.cpp
+echo "#include <stdio.h>" >> $2/Class1.cpp
+echo "" >> $2/Class1.cpp
+echo "namespace $3 {" >> $2/Class1.cpp
+echo "" >> $2/Class1.cpp
+echo "void Class1::hello()" >> $2/Class1.cpp
+echo "{" >> $2/Class1.cpp
+echo " printf(\"Hello friends! :^)\\n\");" >> $2/Class1.cpp
+echo "}" >> $2/Class1.cpp
+echo "" >> $2/Class1.cpp
+echo "}" >> $2/Class1.cpp
+echo "" >> $2/Class1.cpp
diff --git a/Base/res/devel/templates/empty.ini b/Base/res/devel/templates/empty.ini
new file mode 100644
index 0000000000..4c32529c0b
--- /dev/null
+++ b/Base/res/devel/templates/empty.ini
@@ -0,0 +1,5 @@
+[HackStudioTemplate]
+Name=Empty Project
+Description=Template for creating an empty project with no files.
+Priority=100
+IconName32x=empty
diff --git a/Base/res/icons/hackstudio/templates-32x32/cpp-basic.png b/Base/res/icons/hackstudio/templates-32x32/cpp-basic.png
new file mode 100644
index 0000000000..96995229f2
--- /dev/null
+++ b/Base/res/icons/hackstudio/templates-32x32/cpp-basic.png
Binary files differ
diff --git a/Base/res/icons/hackstudio/templates-32x32/cpp-gui.png b/Base/res/icons/hackstudio/templates-32x32/cpp-gui.png
new file mode 100644
index 0000000000..bdbb0a0130
--- /dev/null
+++ b/Base/res/icons/hackstudio/templates-32x32/cpp-gui.png
Binary files differ
diff --git a/Base/res/icons/hackstudio/templates-32x32/cpp-library.png b/Base/res/icons/hackstudio/templates-32x32/cpp-library.png
new file mode 100644
index 0000000000..e00ce34f16
--- /dev/null
+++ b/Base/res/icons/hackstudio/templates-32x32/cpp-library.png
Binary files differ
diff --git a/Base/res/icons/hackstudio/templates-32x32/empty.png b/Base/res/icons/hackstudio/templates-32x32/empty.png
new file mode 100644
index 0000000000..1ca82691cb
--- /dev/null
+++ b/Base/res/icons/hackstudio/templates-32x32/empty.png
Binary files differ
diff --git a/Meta/build-root-filesystem.sh b/Meta/build-root-filesystem.sh
index 339de30812..fc3280cce4 100755
--- a/Meta/build-root-filesystem.sh
+++ b/Meta/build-root-filesystem.sh
@@ -62,6 +62,7 @@ chmod 4750 mnt/bin/keymap
chown 0:$utmp_gid mnt/bin/utmpupdate
chmod 2755 mnt/bin/utmpupdate
chmod 600 mnt/etc/shadow
+chmod 755 mnt/res/devel/templates/*.postcreate
echo "done"
printf "creating initial filesystem structure... "
diff --git a/Userland/DevTools/HackStudio/CMakeLists.txt b/Userland/DevTools/HackStudio/CMakeLists.txt
index e31a214a62..7f4a87858b 100644
--- a/Userland/DevTools/HackStudio/CMakeLists.txt
+++ b/Userland/DevTools/HackStudio/CMakeLists.txt
@@ -1,6 +1,8 @@
add_subdirectory(LanguageServers)
add_subdirectory(LanguageClients)
+compile_gml(Dialogs/NewProjectDialog.gml Dialogs/NewProjectDialogGML.h new_project_dialog_gml)
+
set(SOURCES
CodeDocument.cpp
CursorTool.cpp
@@ -11,6 +13,9 @@ set(SOURCES
Debugger/DisassemblyWidget.cpp
Debugger/RegistersModel.cpp
Debugger/VariablesModel.cpp
+ Dialogs/NewProjectDialog.cpp
+ Dialogs/NewProjectDialogGML.h
+ Dialogs/ProjectTemplatesModel.cpp
Editor.cpp
EditorWrapper.cpp
FindInFilesWidget.cpp
@@ -26,6 +31,7 @@ set(SOURCES
Locator.cpp
Project.cpp
ProjectFile.cpp
+ ProjectTemplate.cpp
TerminalWrapper.cpp
WidgetTool.cpp
WidgetTreeModel.cpp
@@ -33,5 +39,5 @@ set(SOURCES
)
serenity_app(HackStudio ICON app-hack-studio)
-target_link_libraries(HackStudio LibWeb LibMarkdown LibGUI LibCpp LibGfx LibCore LibVT LibDebug LibX86 LibDiff LibShell)
+target_link_libraries(HackStudio LibWeb LibMarkdown LibGUI LibCpp LibGfx LibCore LibVT LibDebug LibX86 LibDiff LibShell LibRegex)
add_dependencies(HackStudio CppLanguageServer)
diff --git a/Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.cpp b/Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.cpp
new file mode 100644
index 0000000000..692804def0
--- /dev/null
+++ b/Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.cpp
@@ -0,0 +1,244 @@
+/*
+ * Copyright (c) 2021, Nick Vella <nick@nxk.io>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "NewProjectDialog.h"
+#include "ProjectTemplatesModel.h"
+#include <DevTools/HackStudio/Dialogs/NewProjectDialogGML.h>
+#include <DevTools/HackStudio/ProjectTemplate.h>
+
+#include <AK/LexicalPath.h>
+#include <AK/String.h>
+#include <LibCore/File.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/IconView.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/RadioButton.h>
+#include <LibGUI/TextBox.h>
+#include <LibGUI/Widget.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/FontDatabase.h>
+#include <LibRegex/Regex.h>
+
+namespace HackStudio {
+
+static const Regex<PosixExtended> s_project_name_validity_regex("^([A-Za-z0-9_-])*$");
+
+int NewProjectDialog::show(GUI::Window* parent_window)
+{
+ auto dialog = NewProjectDialog::construct(parent_window);
+
+ if (parent_window)
+ dialog->set_icon(parent_window->icon());
+
+ auto result = dialog->exec();
+
+ return result;
+}
+
+NewProjectDialog::NewProjectDialog(GUI::Window* parent)
+ : Dialog(parent)
+ , m_model(ProjectTemplatesModel::create())
+{
+ resize(500, 385);
+ center_on_screen();
+ set_resizable(false);
+ set_modal(true);
+ set_title("New project");
+
+ auto& main_widget = set_main_widget<GUI::Widget>();
+ main_widget.load_from_gml(new_project_dialog_gml);
+
+ m_icon_view_container = *main_widget.find_descendant_of_type_named<GUI::Widget>("icon_view_container");
+ m_icon_view = m_icon_view_container->add<GUI::IconView>();
+ m_icon_view->set_always_wrap_item_labels(true);
+ m_icon_view->set_model(m_model);
+ m_icon_view->set_model_column(ProjectTemplatesModel::Column::Name);
+ m_icon_view->on_selection_change = [&]() {
+ update_dialog();
+ };
+ m_icon_view->on_activation = [&]() {
+ if (m_input_valid)
+ do_create_project();
+ };
+
+ m_description_label = *main_widget.find_descendant_of_type_named<GUI::Label>("description_label");
+ m_name_input = *main_widget.find_descendant_of_type_named<GUI::TextBox>("name_input");
+ m_name_input->on_change = [&]() {
+ update_dialog();
+ };
+ m_name_input->on_return_pressed = [&]() {
+ if (m_input_valid)
+ do_create_project();
+ };
+ m_create_in_input = *main_widget.find_descendant_of_type_named<GUI::TextBox>("create_in_input");
+ m_create_in_input->on_change = [&]() {
+ update_dialog();
+ };
+ m_create_in_input->on_return_pressed = [&]() {
+ if (m_input_valid)
+ do_create_project();
+ };
+ m_full_path_label = *main_widget.find_descendant_of_type_named<GUI::Label>("full_path_label");
+
+ m_ok_button = *main_widget.find_descendant_of_type_named<GUI::Button>("ok_button");
+ m_ok_button->on_click = [this](auto) {
+ do_create_project();
+ };
+
+ m_cancel_button = *main_widget.find_descendant_of_type_named<GUI::Button>("cancel_button");
+ m_cancel_button->on_click = [this](auto) {
+ done(ExecResult::ExecCancel);
+ };
+
+ m_browse_button = *find_descendant_of_type_named<GUI::Button>("browse_button");
+ m_browse_button->on_click = [this](auto) {
+ Optional<String> path = GUI::FilePicker::get_open_filepath(this);
+ if (path.has_value())
+ m_create_in_input->set_text(path.value().view());
+ };
+}
+
+NewProjectDialog::~NewProjectDialog()
+{
+}
+
+RefPtr<ProjectTemplate> NewProjectDialog::selected_template()
+{
+ if (m_icon_view->selection().is_empty()) {
+ return {};
+ }
+
+ auto project_template = m_model->template_for_index(m_icon_view->selection().first());
+ ASSERT(!project_template.is_null());
+
+ return project_template;
+}
+
+void NewProjectDialog::update_dialog()
+{
+ auto project_template = selected_template();
+ m_input_valid = true;
+
+ if (project_template) {
+ m_description_label->set_text(project_template->description());
+ } else {
+ m_description_label->set_text("Select a project template to continue.");
+ m_input_valid = false;
+ }
+
+ auto maybe_project_path = get_project_full_path();
+
+ if (maybe_project_path.has_value()) {
+ m_full_path_label->set_text(maybe_project_path.value());
+ } else {
+ m_full_path_label->set_text("Invalid name or creation directory.");
+ m_input_valid = false;
+ }
+
+ m_ok_button->set_enabled(m_input_valid);
+}
+
+Optional<String> NewProjectDialog::get_available_project_name()
+{
+ auto create_in = m_create_in_input->text();
+ auto chosen_name = m_name_input->text();
+
+ // Ensure project name isn't empty or entirely whitespace
+ if (chosen_name.is_empty() || chosen_name.is_whitespace())
+ return {};
+
+ // Validate project name with validity regex
+ if (!s_project_name_validity_regex.has_match(chosen_name))
+ return {};
+
+ if (!Core::File::exists(create_in) || !Core::File::is_directory(create_in))
+ return {};
+
+ // Check for up-to 999 variations of the project name, in case it's already taken
+ for (int i = 0; i < 1000; i++) {
+ auto candidate = (i == 0)
+ ? chosen_name
+ : String::formatted("{}-{}", chosen_name, i);
+
+ if (!Core::File::exists(String::formatted("{}/{}", create_in, candidate)))
+ return candidate;
+ }
+
+ return {};
+}
+
+Optional<String> NewProjectDialog::get_project_full_path()
+{
+ // Do not permit forward-slashes in project names
+ if (m_name_input->text().contains("/"))
+ return {};
+
+ auto create_in = m_create_in_input->text();
+ auto maybe_project_name = get_available_project_name();
+
+ if (!maybe_project_name.has_value()) {
+ return {};
+ }
+
+ auto project_name = maybe_project_name.value();
+ auto full_path = LexicalPath(String::formatted("{}/{}", create_in, project_name));
+
+ // Do not permit otherwise invalid paths.
+ if (!full_path.is_valid())
+ return {};
+
+ return full_path.string();
+}
+
+void NewProjectDialog::do_create_project()
+{
+ auto project_template = selected_template();
+ if (!project_template) {
+ GUI::MessageBox::show_error(this, "Could not create project: no template selected.");
+ return;
+ }
+
+ auto maybe_project_name = get_available_project_name();
+ auto maybe_project_full_path = get_project_full_path();
+ if (!maybe_project_name.has_value() || !maybe_project_full_path.has_value()) {
+ GUI::MessageBox::show_error(this, "Could not create project: invalid project name or path.");
+ return;
+ }
+
+ auto creation_result = project_template->create_project(maybe_project_name.value(), maybe_project_full_path.value());
+ if (!creation_result.is_error()) {
+ // Succesfully created, attempt to open the new project
+ m_created_project_path = maybe_project_full_path.value();
+ done(ExecResult::ExecOK);
+ } else {
+ GUI::MessageBox::show_error(this, String::formatted("Could not create project: {}", creation_result.error()));
+ }
+}
+
+}
diff --git a/Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.gml b/Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.gml
new file mode 100644
index 0000000000..31e461ef72
--- /dev/null
+++ b/Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.gml
@@ -0,0 +1,115 @@
+@GUI::Widget {
+ fill_with_background_color: true
+
+ layout: @GUI::VerticalBoxLayout {
+ margins: [4, 4, 4, 4]
+ }
+
+ @GUI::Label {
+ text: "Templates:"
+ text_alignment: "CenterLeft"
+ max_height: 20
+ }
+
+ @GUI::Widget {
+ layout: @GUI::VerticalBoxLayout {
+ }
+
+ name: "icon_view_container"
+ }
+
+ @GUI::Label {
+ name: "description_label"
+ text_alignment: "CenterLeft"
+ thickness: 2
+ shadow: "Sunken"
+ shape: "Container"
+ max_height: 24
+ }
+
+ @GUI::Widget {
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ max_height: 24
+
+ @GUI::Label {
+ text: "Name:"
+ text_alignment: "CenterLeft"
+ max_width: 75
+ }
+
+ @GUI::TextBox {
+ name: "name_input"
+ }
+ }
+
+ @GUI::Widget {
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ max_height: 24
+
+ @GUI::Label {
+ text: "Create in:"
+ text_alignment: "CenterLeft"
+ max_width: 75
+ }
+
+ @GUI::TextBox {
+ name: "create_in_input"
+ text: "/home/anon/Source"
+ }
+
+ @GUI::Button {
+ name: "browse_button"
+ text: "Browse"
+ max_width: 75
+ }
+ }
+
+ @GUI::Widget {
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ max_height: 24
+
+ @GUI::Label {
+ text: "Full path:"
+ text_alignment: "CenterLeft"
+ max_width: 75
+ }
+
+ @GUI::Label {
+ name: "full_path_label"
+ text_alignment: "CenterLeft"
+ text: ""
+ thickness: 2
+ shadow: "Sunken"
+ shape: "Container"
+ max_height: 22
+ }
+ }
+
+ @GUI::Widget {
+ layout: @GUI::HorizontalBoxLayout {
+ }
+
+ max_height: 24
+
+ @GUI::Widget {
+ }
+
+ @GUI::Button {
+ name: "ok_button"
+ text: "OK"
+ max_width: 75
+ }
+
+ @GUI::Button {
+ name: "cancel_button"
+ text: "Cancel"
+ max_width: 75
+ }
+ }
+}
diff --git a/Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.h b/Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.h
new file mode 100644
index 0000000000..22c3827ac5
--- /dev/null
+++ b/Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2021, Nick Vella <nick@nxk.io>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "ProjectTemplatesModel.h"
+#include <DevTools/HackStudio/ProjectTemplate.h>
+
+#include <AK/Result.h>
+#include <AK/Vector.h>
+#include <LibGUI/Button.h>
+#include <LibGUI/Dialog.h>
+#include <LibGUI/Label.h>
+#include <LibGUI/TextBox.h>
+
+namespace HackStudio {
+
+class NewProjectDialog : public GUI::Dialog {
+ C_OBJECT(NewProjectDialog);
+
+public:
+ static int show(GUI::Window* parent_window);
+
+ Optional<String> created_project_path() const { return m_created_project_path; }
+
+private:
+ NewProjectDialog(GUI::Window* parent);
+ virtual ~NewProjectDialog() override;
+
+ void update_dialog();
+ Optional<String> get_available_project_name();
+ Optional<String> get_project_full_path();
+
+ void do_create_project();
+
+ RefPtr<ProjectTemplate> selected_template();
+
+ NonnullRefPtr<ProjectTemplatesModel> m_model;
+ bool m_input_valid { false };
+
+ RefPtr<GUI::Widget> m_icon_view_container;
+ RefPtr<GUI::IconView> m_icon_view;
+
+ RefPtr<GUI::Label> m_description_label;
+ RefPtr<GUI::TextBox> m_name_input;
+ RefPtr<GUI::TextBox> m_create_in_input;
+ RefPtr<GUI::Label> m_full_path_label;
+
+ RefPtr<GUI::Button> m_ok_button;
+ RefPtr<GUI::Button> m_cancel_button;
+ RefPtr<GUI::Button> m_browse_button;
+
+ Optional<String> m_created_project_path;
+};
+
+}
diff --git a/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp b/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp
new file mode 100644
index 0000000000..a2dd21e0b1
--- /dev/null
+++ b/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2021, Nick Vella <nick@nxk.io>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ProjectTemplatesModel.h"
+
+#include <AK/LexicalPath.h>
+#include <AK/QuickSort.h>
+#include <LibCore/DirIterator.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Variant.h>
+#include <LibGfx/TextAlignment.h>
+#include <ctype.h>
+#include <stdio.h>
+
+namespace HackStudio {
+
+ProjectTemplatesModel::ProjectTemplatesModel()
+ : m_templates()
+ , m_mapping()
+{
+ auto watcher_or_error = Core::FileWatcher::watch(ProjectTemplate::templates_path());
+ if (!watcher_or_error.is_error()) {
+ m_file_watcher = watcher_or_error.release_value();
+ m_file_watcher->on_change = [&](auto) {
+ update();
+ };
+ } else {
+ warnln("Unable to watch templates directory, templates will not automatically refresh. Error: {}", watcher_or_error.error());
+ }
+
+ rescan_templates();
+}
+
+ProjectTemplatesModel::~ProjectTemplatesModel()
+{
+}
+
+int ProjectTemplatesModel::row_count(const GUI::ModelIndex&) const
+{
+ return m_mapping.size();
+}
+
+int ProjectTemplatesModel::column_count(const GUI::ModelIndex&) const
+{
+ return Column::__Count;
+}
+
+String ProjectTemplatesModel::column_name(int column) const
+{
+ switch (column) {
+ case Column::Icon:
+ return "Icon";
+ case Column::Id:
+ return "ID";
+ case Column::Name:
+ return "Name";
+ }
+ ASSERT_NOT_REACHED();
+}
+
+GUI::Variant ProjectTemplatesModel::data(const GUI::ModelIndex& index, GUI::ModelRole role) const
+{
+ if (static_cast<size_t>(index.row()) >= m_mapping.size())
+ return {};
+
+ if (role == GUI::ModelRole::TextAlignment)
+ return Gfx::TextAlignment::CenterLeft;
+
+ if (role == GUI::ModelRole::Display) {
+ switch (index.column()) {
+ case Column::Name:
+ return m_mapping[index.row()]->name();
+ case Column::Id:
+ return m_mapping[index.row()]->id();
+ }
+ }
+
+ if (role == GUI::ModelRole::Icon) {
+ return m_mapping[index.row()]->icon();
+ }
+
+ return {};
+}
+
+RefPtr<ProjectTemplate> ProjectTemplatesModel::template_for_index(const GUI::ModelIndex& index)
+{
+ if (static_cast<size_t>(index.row()) >= m_mapping.size())
+ return {};
+
+ return m_mapping[index.row()];
+}
+
+void ProjectTemplatesModel::update()
+{
+ rescan_templates();
+ did_update();
+}
+
+void ProjectTemplatesModel::rescan_templates()
+{
+ m_templates.clear();
+
+ // Iterate over template manifest INI files in the templates path
+ Core::DirIterator di(ProjectTemplate::templates_path(), Core::DirIterator::SkipDots);
+ if (di.has_error()) {
+ warnln("DirIterator: {}", di.error_string());
+ return;
+ }
+
+ while (di.has_next()) {
+ auto full_path = LexicalPath(di.next_full_path());
+ if (!full_path.has_extension(".ini"))
+ continue;
+
+ auto project_template = ProjectTemplate::load_from_manifest(full_path.string());
+ if (!project_template) {
+ warnln("Template manifest {} is invalid.", full_path.string());
+ continue;
+ }
+
+ m_templates.append(project_template.release_nonnull());
+ }
+
+ // Enumerate the loaded projects into a sorted mapping, by priority value descending.
+ m_mapping.clear();
+ for (auto& project_template : m_templates)
+ m_mapping.append(&project_template);
+ quick_sort(m_mapping, [](auto a, auto b) {
+ return a->priority() > b->priority();
+ });
+}
+
+}
diff --git a/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.h b/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.h
new file mode 100644
index 0000000000..a53714366c
--- /dev/null
+++ b/Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2021, Nick Vella <nick@nxk.io>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/NonnullPtrVector.h>
+#include <AK/RefPtr.h>
+#include <AK/WeakPtr.h>
+#include <DevTools/HackStudio/ProjectTemplate.h>
+#include <LibCore/FileWatcher.h>
+#include <LibGUI/Model.h>
+
+namespace HackStudio {
+
+class ProjectTemplatesModel final : public GUI::Model {
+public:
+ static NonnullRefPtr<ProjectTemplatesModel> create()
+ {
+ return adopt(*new ProjectTemplatesModel());
+ }
+
+ enum Column {
+ Icon = 0,
+ Id,
+ Name,
+ __Count
+ };
+
+ virtual ~ProjectTemplatesModel() override;
+
+ RefPtr<ProjectTemplate> template_for_index(const GUI::ModelIndex& index);
+
+ virtual int row_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
+ virtual int column_count(const GUI::ModelIndex& = GUI::ModelIndex()) const override;
+ virtual String column_name(int) const override;
+ virtual GUI::Variant data(const GUI::ModelIndex&, GUI::ModelRole) const override;
+ virtual void update() override;
+
+ void rescan_templates();
+
+private:
+ explicit ProjectTemplatesModel();
+
+ NonnullRefPtrVector<ProjectTemplate> m_templates;
+ Vector<ProjectTemplate*> m_mapping;
+
+ RefPtr<Core::FileWatcher> m_file_watcher;
+};
+
+}
diff --git a/Userland/DevTools/HackStudio/HackStudioWidget.cpp b/Userland/DevTools/HackStudio/HackStudioWidget.cpp
index 06c62c4510..e492cc3647 100644
--- a/Userland/DevTools/HackStudio/HackStudioWidget.cpp
+++ b/Userland/DevTools/HackStudio/HackStudioWidget.cpp
@@ -31,6 +31,7 @@
#include "Debugger/DebugInfoWidget.h"
#include "Debugger/Debugger.h"
#include "Debugger/DisassemblyWidget.h"
+#include "Dialogs/NewProjectDialog.h"
#include "Editor.h"
#include "EditorWrapper.h"
#include "FindInFilesWidget.h"
@@ -57,6 +58,7 @@
#include <LibGUI/Application.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
+#include <LibGUI/Dialog.h>
#include <LibGUI/EditingEngine.h>
#include <LibGUI/FilePicker.h>
#include <LibGUI/InputBox.h>
@@ -130,6 +132,7 @@ HackStudioWidget::HackStudioWidget(const String& path_to_project)
m_remove_current_editor_action = create_remove_current_editor_action();
m_open_action = create_open_action();
m_save_action = create_save_action();
+ m_new_project_action = create_new_project_action();
create_action_tab(*m_right_hand_splitter);
@@ -383,6 +386,18 @@ NonnullRefPtr<GUI::Action> HackStudioWidget::create_delete_action()
return delete_action;
}
+NonnullRefPtr<GUI::Action> HackStudioWidget::create_new_project_action()
+{
+ return GUI::Action::create("Create new project...", { Mod_Ctrl | Mod_Shift, Key_N }, Gfx::Bitmap::load_from_file("/res/icons/16x16/mkdir.png"), [this](const GUI::Action&) {
+ auto dialog = NewProjectDialog::construct(window());
+ dialog->set_icon(window()->icon());
+ auto result = dialog->exec();
+
+ if (result == GUI::Dialog::ExecResult::ExecOK && dialog->created_project_path().has_value())
+ open_project(dialog->created_project_path().value());
+ });
+}
+
void HackStudioWidget::add_new_editor(GUI::Widget& parent)
{
auto wrapper = EditorWrapper::construct();
@@ -849,6 +864,7 @@ void HackStudioWidget::create_action_tab(GUI::Widget& parent)
void HackStudioWidget::create_app_menubar(GUI::MenuBar& menubar)
{
auto& app_menu = menubar.add_menu("Hack Studio");
+ app_menu.add_action(*m_new_project_action);
app_menu.add_action(*m_open_action);
app_menu.add_action(*m_save_action);
app_menu.add_separator();
diff --git a/Userland/DevTools/HackStudio/HackStudioWidget.h b/Userland/DevTools/HackStudio/HackStudioWidget.h
index e5728bfd17..ef43639357 100644
--- a/Userland/DevTools/HackStudio/HackStudioWidget.h
+++ b/Userland/DevTools/HackStudio/HackStudioWidget.h
@@ -84,6 +84,7 @@ private:
NonnullRefPtr<GUI::Action> create_new_directory_action();
NonnullRefPtr<GUI::Action> create_open_selected_action();
NonnullRefPtr<GUI::Action> create_delete_action();
+ NonnullRefPtr<GUI::Action> create_new_project_action();
NonnullRefPtr<GUI::Action> create_switch_to_next_editor_action();
NonnullRefPtr<GUI::Action> create_switch_to_previous_editor_action();
NonnullRefPtr<GUI::Action> create_remove_current_editor_action();
@@ -158,6 +159,7 @@ private:
RefPtr<GUI::Action> m_new_directory_action;
RefPtr<GUI::Action> m_open_selected_action;
RefPtr<GUI::Action> m_delete_action;
+ RefPtr<GUI::Action> m_new_project_action;
RefPtr<GUI::Action> m_switch_to_next_editor;
RefPtr<GUI::Action> m_switch_to_previous_editor;
RefPtr<GUI::Action> m_remove_current_editor_action;
diff --git a/Userland/DevTools/HackStudio/ProjectTemplate.cpp b/Userland/DevTools/HackStudio/ProjectTemplate.cpp
new file mode 100644
index 0000000000..5b2c413980
--- /dev/null
+++ b/Userland/DevTools/HackStudio/ProjectTemplate.cpp
@@ -0,0 +1,279 @@
+/*
+ * Copyright (c) 2021, Nick Vella <nick@nxk.io>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ProjectTemplate.h"
+#include <AK/LexicalPath.h>
+#include <AK/String.h>
+#include <AK/StringBuilder.h>
+#include <LibCore/ArgsParser.h>
+#include <LibCore/ConfigFile.h>
+#include <LibCore/DirIterator.h>
+#include <LibCore/File.h>
+#include <assert.h>
+#include <fcntl.h>
+#include <spawn.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+// FIXME: shameless copy+paste from Userland/cp. We should have system-wide file management functions.
+// Issue #5209
+bool copy_file_or_directory(String, String, bool, bool);
+bool copy_file(String, String, const struct stat&, int);
+bool copy_directory(String, String, bool);
+
+namespace HackStudio {
+
+ProjectTemplate::ProjectTemplate(const String& id, const String& name, const String& description, const GUI::Icon& icon, int priority)
+ : m_id(id)
+ , m_name(name)
+ , m_description(description)
+ , m_icon(icon)
+ , m_priority(priority)
+{
+}
+
+RefPtr<ProjectTemplate> ProjectTemplate::load_from_manifest(const String& manifest_path)
+{
+ auto config = Core::ConfigFile::open(manifest_path);
+
+ if (!config->has_group("HackStudioTemplate")
+ || !config->has_key("HackStudioTemplate", "Name")
+ || !config->has_key("HackStudioTemplate", "Description")
+ || !config->has_key("HackStudioTemplate", "IconName32x"))
+ return {};
+
+ auto id = LexicalPath(manifest_path).title();
+ auto name = config->read_entry("HackStudioTemplate", "Name");
+ auto description = config->read_entry("HackStudioTemplate", "Description");
+ int priority = config->read_num_entry("HackStudioTemplate", "Priority", 0);
+
+ // Attempt to read in the template icons
+ // Fallback to a generic executable icon if one isn't found
+ auto icon = GUI::Icon::default_icon("filetype-executable");
+
+ auto bitmap_path_32 = String::formatted("/res/icons/hackstudio/templates-32x32/{}.png", config->read_entry("HackStudioTemplate", "IconName32x"));
+
+ if (Core::File::exists(bitmap_path_32)) {
+ auto bitmap32 = Gfx::Bitmap::load_from_file(bitmap_path_32);
+ icon = GUI::Icon(move(bitmap32));
+ }
+
+ return adopt(*new ProjectTemplate(id, name, description, icon, priority));
+}
+
+Result<void, String> ProjectTemplate::create_project(const String& name, const String& path)
+{
+ // Check if a file or directory already exists at the project path
+ if (Core::File::exists(path))
+ return String("File or directory already exists at specified location.");
+
+ dbgln("Creating project at path '{}' with name '{}'", path, name);
+
+ // Verify that the template content directory exists. If it does, copy it's contents.
+ // Otherwise, create an empty directory at the project path.
+ if (Core::File::is_directory(content_path())) {
+ if (!copy_directory(content_path(), path, false))
+ return String("Failed to copy template contents.");
+ } else {
+ dbgln("No template content directory found for '{}', creating an empty directory for the project.", m_id);
+ int rc;
+ if ((rc = mkdir(path.characters(), 0755)) < 0) {
+ return String::formatted("Failed to mkdir empty project directory, error: {}, rc: {}.", strerror(errno), rc);
+ }
+ }
+
+ // Check for an executable post-create script in $TEMPLATES_DIR/$ID.postcreate,
+ // and run it with the path and name
+
+ auto postcreate_script_path = LexicalPath::canonicalized_path(String::formatted("{}/{}.postcreate", templates_path(), m_id));
+ struct stat postcreate_st;
+ int result = stat(postcreate_script_path.characters(), &postcreate_st);
+ if (result == 0 && (postcreate_st.st_mode & S_IXOTH) == S_IXOTH) {
+ dbgln("Running post-create script '{}'", postcreate_script_path);
+
+ // Generate a namespace-safe project name (replace hyphens with underscores)
+ String namespace_safe(name.characters());
+ namespace_safe.replace("-", "_", true);
+
+ pid_t child_pid;
+ const char* argv[] = { postcreate_script_path.characters(), name.characters(), path.characters(), namespace_safe.characters(), nullptr };
+
+ if ((errno = posix_spawn(&child_pid, postcreate_script_path.characters(), nullptr, nullptr, const_cast<char**>(argv), environ))) {
+ perror("posix_spawn");
+ return String("Failed to spawn project post-create script.");
+ }
+
+ // Command spawned, wait for exit.
+ int status;
+ if (waitpid(child_pid, &status, 0) < 0)
+ return String("Failed to spawn project post-create script.");
+
+ int child_error = WEXITSTATUS(status);
+ dbgln("Post-create script exited with code {}", child_error);
+
+ if (child_error != 0)
+ return String("Project post-creation script exited with non-zero error code.");
+ }
+
+ return {};
+}
+
+}
+
+// FIXME: shameless copy+paste from Userland/cp. We should have system-wide file management functions.
+// Issue #5209
+bool copy_file_or_directory(String src_path, String dst_path, bool recursion_allowed, bool link)
+{
+ int src_fd = open(src_path.characters(), O_RDONLY);
+ if (src_fd < 0) {
+ perror("open src");
+ return false;
+ }
+
+ struct stat src_stat;
+ int rc = fstat(src_fd, &src_stat);
+ if (rc < 0) {
+ perror("stat src");
+ return false;
+ }
+
+ if (S_ISDIR(src_stat.st_mode)) {
+ if (!recursion_allowed) {
+ fprintf(stderr, "cp: -R not specified; omitting directory '%s'\n", src_path.characters());
+ return false;
+ }
+ return copy_directory(src_path, dst_path, link);
+ }
+ if (link) {
+ if (::link(src_path.characters(), dst_path.characters()) < 0) {
+ perror("link");
+ return false;
+ }
+ return true;
+ }
+
+ return copy_file(src_path, dst_path, src_stat, src_fd);
+}
+
+bool copy_file(String src_path, String dst_path, const struct stat& src_stat, int src_fd)
+{
+ // Get umask
+ auto my_umask = umask(0);
+ umask(my_umask);
+
+ // NOTE: We don't copy the set-uid and set-gid bits.
+ mode_t mode = (src_stat.st_mode & ~my_umask) & ~06000;
+
+ int dst_fd = creat(dst_path.characters(), mode);
+ if (dst_fd < 0) {
+ if (errno != EISDIR) {
+ perror("open dst");
+ return false;
+ }
+ StringBuilder builder;
+ builder.append(dst_path);
+ builder.append('/');
+ builder.append(LexicalPath(src_path).basename());
+ dst_path = builder.to_string();
+ dst_fd = creat(dst_path.characters(), 0666);
+ if (dst_fd < 0) {
+ perror("open dst");
+ return false;
+ }
+ }
+
+ if (src_stat.st_size > 0) {
+ if (ftruncate(dst_fd, src_stat.st_size) < 0) {
+ perror("cp: ftruncate");
+ return false;
+ }
+ }
+
+ for (;;) {
+ char buffer[32768];
+ ssize_t nread = read(src_fd, buffer, sizeof(buffer));
+ if (nread < 0) {
+ perror("read src");
+ return false;
+ }
+ if (nread == 0)
+ break;
+ ssize_t remaining_to_write = nread;
+ char* bufptr = buffer;
+ while (remaining_to_write) {
+ ssize_t nwritten = write(dst_fd, bufptr, remaining_to_write);
+ if (nwritten < 0) {
+ perror("write dst");
+ return false;
+ }
+ assert(nwritten > 0);
+ remaining_to_write -= nwritten;
+ bufptr += nwritten;
+ }
+ }
+
+ close(src_fd);
+ close(dst_fd);
+ return true;
+}
+
+bool copy_directory(String src_path, String dst_path, bool link)
+{
+ int rc = mkdir(dst_path.characters(), 0755);
+ if (rc < 0) {
+ perror("cp: mkdir");
+ return false;
+ }
+
+ String src_rp = Core::File::real_path_for(src_path);
+ src_rp = String::format("%s/", src_rp.characters());
+ String dst_rp = Core::File::real_path_for(dst_path);
+ dst_rp = String::format("%s/", dst_rp.characters());
+
+ if (!dst_rp.is_empty() && dst_rp.starts_with(src_rp)) {
+ fprintf(stderr, "cp: Cannot copy %s into itself (%s)\n",
+ src_path.characters(), dst_path.characters());
+ return false;
+ }
+
+ Core::DirIterator di(src_path, Core::DirIterator::SkipDots);
+ if (di.has_error()) {
+ fprintf(stderr, "cp: DirIterator: %s\n", di.error_string());
+ return false;
+ }
+ while (di.has_next()) {
+ String filename = di.next_path();
+ bool is_copied = copy_file_or_directory(
+ String::format("%s/%s", src_path.characters(), filename.characters()),
+ String::format("%s/%s", dst_path.characters(), filename.characters()),
+ true, link);
+ if (!is_copied) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/Userland/DevTools/HackStudio/ProjectTemplate.h b/Userland/DevTools/HackStudio/ProjectTemplate.h
new file mode 100644
index 0000000000..99c1f757b6
--- /dev/null
+++ b/Userland/DevTools/HackStudio/ProjectTemplate.h
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2021, Nick Vella <nick@nxk.io>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <AK/ByteBuffer.h>
+#include <AK/LexicalPath.h>
+#include <AK/RefCounted.h>
+#include <AK/Result.h>
+#include <AK/String.h>
+#include <AK/Weakable.h>
+#include <LibGUI/Icon.h>
+
+namespace HackStudio {
+
+class ProjectTemplate : public RefCounted<ProjectTemplate> {
+public:
+ static String templates_path() { return "/res/devel/templates"; }
+
+ static RefPtr<ProjectTemplate> load_from_manifest(const String& manifest_path);
+
+ explicit ProjectTemplate(const String& id, const String& name, const String& description, const GUI::Icon& icon, int priority);
+
+ Result<void, String> create_project(const String& name, const String& path);
+
+ const String& id() const { return m_id; }
+ const String& name() const { return m_name; }
+ const String& description() const { return m_description; }
+ const GUI::Icon& icon() const { return m_icon; }
+ const String content_path() const
+ {
+ return LexicalPath::canonicalized_path(String::formatted("{}/{}", templates_path(), m_id));
+ }
+ int priority() const { return m_priority; }
+
+private:
+ String m_id;
+ String m_name;
+ String m_description;
+ GUI::Icon m_icon;
+ int m_priority { 0 };
+};
+
+}