From b6715772234e3e55235b21fff05487e29727bfe8 Mon Sep 17 00:00:00 2001 From: Nick Vella Date: Sat, 13 Feb 2021 21:22:48 +1100 Subject: [PATCH] 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. --- Base/res/devel/templates/cpp-basic.ini | 5 + Base/res/devel/templates/cpp-basic.postcreate | 19 ++ Base/res/devel/templates/cpp-basic/main.cpp | 7 + Base/res/devel/templates/cpp-gui.ini | 5 + Base/res/devel/templates/cpp-gui.postcreate | 19 ++ Base/res/devel/templates/cpp-gui/main.cpp | 26 ++ Base/res/devel/templates/cpp-library.ini | 4 + .../devel/templates/cpp-library.postcreate | 49 +++ Base/res/devel/templates/empty.ini | 5 + .../hackstudio/templates-32x32/cpp-basic.png | Bin 0 -> 511 bytes .../hackstudio/templates-32x32/cpp-gui.png | Bin 0 -> 2559 bytes .../templates-32x32/cpp-library.png | Bin 0 -> 2933 bytes .../hackstudio/templates-32x32/empty.png | Bin 0 -> 6008 bytes Meta/build-root-filesystem.sh | 1 + Userland/DevTools/HackStudio/CMakeLists.txt | 8 +- .../HackStudio/Dialogs/NewProjectDialog.cpp | 244 +++++++++++++++ .../HackStudio/Dialogs/NewProjectDialog.gml | 115 ++++++++ .../HackStudio/Dialogs/NewProjectDialog.h | 79 +++++ .../Dialogs/ProjectTemplatesModel.cpp | 156 ++++++++++ .../Dialogs/ProjectTemplatesModel.h | 73 +++++ .../DevTools/HackStudio/HackStudioWidget.cpp | 16 + .../DevTools/HackStudio/HackStudioWidget.h | 2 + .../DevTools/HackStudio/ProjectTemplate.cpp | 279 ++++++++++++++++++ .../DevTools/HackStudio/ProjectTemplate.h | 67 +++++ 24 files changed, 1178 insertions(+), 1 deletion(-) create mode 100644 Base/res/devel/templates/cpp-basic.ini create mode 100644 Base/res/devel/templates/cpp-basic.postcreate create mode 100644 Base/res/devel/templates/cpp-basic/main.cpp create mode 100644 Base/res/devel/templates/cpp-gui.ini create mode 100644 Base/res/devel/templates/cpp-gui.postcreate create mode 100644 Base/res/devel/templates/cpp-gui/main.cpp create mode 100644 Base/res/devel/templates/cpp-library.ini create mode 100644 Base/res/devel/templates/cpp-library.postcreate create mode 100644 Base/res/devel/templates/empty.ini create mode 100644 Base/res/icons/hackstudio/templates-32x32/cpp-basic.png create mode 100644 Base/res/icons/hackstudio/templates-32x32/cpp-gui.png create mode 100644 Base/res/icons/hackstudio/templates-32x32/cpp-library.png create mode 100644 Base/res/icons/hackstudio/templates-32x32/empty.png create mode 100644 Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.cpp create mode 100644 Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.gml create mode 100644 Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.h create mode 100644 Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp create mode 100644 Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.h create mode 100644 Userland/DevTools/HackStudio/ProjectTemplate.cpp create mode 100644 Userland/DevTools/HackStudio/ProjectTemplate.h 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 + +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 +#include +#include +#include +#include + +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 " >> $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 0000000000000000000000000000000000000000..96995229f231065f2f54d5cf773409ff3ce70a58 GIT binary patch literal 511 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}jKx9jP7LeL$-HD>U|>mi z^mSxl*x1kgCy|wbfq}EYBeIx*f$uN~Gak=hk;1^hIK|V&F(ktM?KEq@BL)I(ve!;t znsMTv-6ydc+xz-ZB)wXe7)zYXVfd!`@gc!F<2dOdZy3)!QsI3 zK#-QYK!r#S~`D$oJ z)YJ?nmB`uRi#R9fW_)(KqPs$E^|XWc>{WK$E!dcl`BBuyUiN2do|nds{o7pX?#*G~ zt8QE!nz!xKzqU!uvlx;lFz{%{uZ_(VG7xYsxX~QQT42y%`00IXP`se;RYkw%XBQY4 P7#KWV{an^LB{Ts5v3A#< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bdbb0a0130d2d5ecdcf6a39d5079ea180fce9652 GIT binary patch literal 2559 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%0D!H;oQdaA2H@oTTI^mM$eQdegIuhDnoybUj z8l2Ml=fSIIN3XvY+}o0su5S>im7T;9VvyZlm@8}8S7p)j@BNZEL4yms7&rIoY;)fz zQ(`3+7~?q8X~pZV7cr-@s%tKXZ|!|M?YV*suk${eKRGrrU%lFneq0lKF`##M(DdD% z85bV;wLE?rkZE=?cJ-Trw@r!dLUJk_8}}cq|Fb%J`g0kM>3!vTYYX!97jr_vcA%Nk{G_tJekA^7>tRp=WY;cjW42zuU@I#k|;OaXxy> z+{!P#`WODOl@=d*7kA;G`CI|pPYCo?cq%qg8{?ak~c(rPcPDkRFS>*BI4U}Jy)>((m~ zH&{3W4~B2v{Z8$WT4b59$_*|vv8L9H$E!ccXK3Hh;%HLpi1@)>W2*F8R@p^mX6m=P zIWvoAS~4%(k(~DD7UPMO&v8XY%g&zF&zP8gs6>d>t>kj~ap6vj(BsE-O;_FfvO4bS z_fQ$$)6?>ueD{9X@Up7%q2a?HS@%NsXx_1{C5w;vcLl4~1;06~ zqOP!{Dr=>E&Ywi%x2Y}Ft-Fo<4*s$2-{rY@>fE{0Zs~1ip06_b$OQw94NcDT>Q+Xc zT(c?v!Etj}KbvKLV!J9Hc}x_`pTC0jWtQhw*X>V=HCEmE|NP}`=jNsFdJR`p^E6%0 z>&Q8LJxriRj_u%e!4~-kHqqant1Q1*esTM$sOxqn>kq$ljWjNMki&fIUF_Y<^-DIK znUH(^W8)tuV3E!}r-FDOd%9ql2`O|;RNz6FFz`($k|H*Yfq{Xuz$3Dlfq`2Hgc&d0t^32kz`$PO>Fdh=l!=j5)M#Tz;%f#5#uiT( z$B>A_Tc>Z#J7gfx_I}Mf4$hAZxl79uQhQjWg}7~e=PwAJzra6GOvUVmV0DYfH@}%y zyO}MPyt|n`U)I}CMDx@Cr|l+zT9u^9$)8D^#sWaoA(DzLb4!vs%5ITKRV4>x5g(+9wvnefm zpuzFx*WOjqn`ZtvFs*y~l*5(*J66IroBW95BI4&`UNZZo`y zUb9Mi)6Fy2#Pg+U%C(qg+|Jr25h`BBwMc<4{(PMEG8wP`Z@J~?^G~s0%E+~f4xduY z_~>ib!uE@L2V(!+>e%|={RM^}F%HWfudm==dxQOG>e Q1_lNOPgg&ebxsLQ01fMpt^fc4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1ca82691cbb38527ea2fa5cf72201a991ceb662e GIT binary patch literal 6008 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 DLZ%3` literal 0 HcmV?d00001 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 + * 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace HackStudio { + +static const Regex 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(); + main_widget.load_from_gml(new_project_dialog_gml); + + m_icon_view_container = *main_widget.find_descendant_of_type_named("icon_view_container"); + m_icon_view = m_icon_view_container->add(); + 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("description_label"); + m_name_input = *main_widget.find_descendant_of_type_named("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("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("full_path_label"); + + m_ok_button = *main_widget.find_descendant_of_type_named("ok_button"); + m_ok_button->on_click = [this](auto) { + do_create_project(); + }; + + m_cancel_button = *main_widget.find_descendant_of_type_named("cancel_button"); + m_cancel_button->on_click = [this](auto) { + done(ExecResult::ExecCancel); + }; + + m_browse_button = *find_descendant_of_type_named("browse_button"); + m_browse_button->on_click = [this](auto) { + Optional path = GUI::FilePicker::get_open_filepath(this); + if (path.has_value()) + m_create_in_input->set_text(path.value().view()); + }; +} + +NewProjectDialog::~NewProjectDialog() +{ +} + +RefPtr 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 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 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 + * 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 + +#include +#include +#include +#include +#include +#include + +namespace HackStudio { + +class NewProjectDialog : public GUI::Dialog { + C_OBJECT(NewProjectDialog); + +public: + static int show(GUI::Window* parent_window); + + Optional created_project_path() const { return m_created_project_path; } + +private: + NewProjectDialog(GUI::Window* parent); + virtual ~NewProjectDialog() override; + + void update_dialog(); + Optional get_available_project_name(); + Optional get_project_full_path(); + + void do_create_project(); + + RefPtr selected_template(); + + NonnullRefPtr m_model; + bool m_input_valid { false }; + + RefPtr m_icon_view_container; + RefPtr m_icon_view; + + RefPtr m_description_label; + RefPtr m_name_input; + RefPtr m_create_in_input; + RefPtr m_full_path_label; + + RefPtr m_ok_button; + RefPtr m_cancel_button; + RefPtr m_browse_button; + + Optional 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 + * 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 +#include +#include +#include +#include +#include +#include +#include + +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(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 ProjectTemplatesModel::template_for_index(const GUI::ModelIndex& index) +{ + if (static_cast(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 + * 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 +#include +#include +#include +#include +#include + +namespace HackStudio { + +class ProjectTemplatesModel final : public GUI::Model { +public: + static NonnullRefPtr create() + { + return adopt(*new ProjectTemplatesModel()); + } + + enum Column { + Icon = 0, + Id, + Name, + __Count + }; + + virtual ~ProjectTemplatesModel() override; + + RefPtr 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 m_templates; + Vector m_mapping; + + RefPtr 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 #include #include +#include #include #include #include @@ -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 HackStudioWidget::create_delete_action() return delete_action; } +NonnullRefPtr 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 create_new_directory_action(); NonnullRefPtr create_open_selected_action(); NonnullRefPtr create_delete_action(); + NonnullRefPtr create_new_project_action(); NonnullRefPtr create_switch_to_next_editor_action(); NonnullRefPtr create_switch_to_previous_editor_action(); NonnullRefPtr create_remove_current_editor_action(); @@ -158,6 +159,7 @@ private: RefPtr m_new_directory_action; RefPtr m_open_selected_action; RefPtr m_delete_action; + RefPtr m_new_project_action; RefPtr m_switch_to_next_editor; RefPtr m_switch_to_previous_editor; RefPtr 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 + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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::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 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(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 + * 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 +#include +#include +#include +#include +#include +#include + +namespace HackStudio { + +class ProjectTemplate : public RefCounted { +public: + static String templates_path() { return "/res/devel/templates"; } + + static RefPtr 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 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 }; +}; + +}