diff options
author | Nick Vella <nick@nxk.io> | 2021-02-13 21:22:48 +1100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2021-02-13 19:49:30 +0100 |
commit | b6715772234e3e55235b21fff05487e29727bfe8 (patch) | |
tree | 39a93db4d872f1de1c04985b11f78009fc02d3cd | |
parent | a6fdc17f3f0add00fd15a1223415e8dd04ceb6f8 (diff) | |
download | serenity-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.
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 Binary files differnew file mode 100644 index 0000000000..96995229f2 --- /dev/null +++ b/Base/res/icons/hackstudio/templates-32x32/cpp-basic.png diff --git a/Base/res/icons/hackstudio/templates-32x32/cpp-gui.png b/Base/res/icons/hackstudio/templates-32x32/cpp-gui.png Binary files differnew file mode 100644 index 0000000000..bdbb0a0130 --- /dev/null +++ b/Base/res/icons/hackstudio/templates-32x32/cpp-gui.png diff --git a/Base/res/icons/hackstudio/templates-32x32/cpp-library.png b/Base/res/icons/hackstudio/templates-32x32/cpp-library.png Binary files differnew file mode 100644 index 0000000000..e00ce34f16 --- /dev/null +++ b/Base/res/icons/hackstudio/templates-32x32/cpp-library.png diff --git a/Base/res/icons/hackstudio/templates-32x32/empty.png b/Base/res/icons/hackstudio/templates-32x32/empty.png Binary files differnew file mode 100644 index 0000000000..1ca82691cb --- /dev/null +++ b/Base/res/icons/hackstudio/templates-32x32/empty.png 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 }; +}; + +} |