mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 15:32:46 +00:00 
			
		
		
		
	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.
This commit is contained in:
		
							parent
							
								
									a6fdc17f3f
								
							
						
					
					
						commit
						b671577223
					
				
					 24 changed files with 1178 additions and 1 deletions
				
			
		
							
								
								
									
										5
									
								
								Base/res/devel/templates/cpp-basic.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Base/res/devel/templates/cpp-basic.ini
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
							
								
								
									
										19
									
								
								Base/res/devel/templates/cpp-basic.postcreate
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Base/res/devel/templates/cpp-basic.postcreate
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
							
								
								
									
										7
									
								
								Base/res/devel/templates/cpp-basic/main.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Base/res/devel/templates/cpp-basic/main.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | #include <stdio.h> | ||||||
|  | 
 | ||||||
|  | int main(int argc, char** argv) | ||||||
|  | { | ||||||
|  |     printf("Hello friends!\n"); | ||||||
|  |     return 0; | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								Base/res/devel/templates/cpp-gui.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Base/res/devel/templates/cpp-gui.ini
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | [HackStudioTemplate] | ||||||
|  | Name=Graphical Application (C++) | ||||||
|  | Description=Template for creating a basic C++ graphical application. | ||||||
|  | Priority=90 | ||||||
|  | IconName32x=cpp-gui | ||||||
							
								
								
									
										19
									
								
								Base/res/devel/templates/cpp-gui.postcreate
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Base/res/devel/templates/cpp-gui.postcreate
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
							
								
								
									
										26
									
								
								Base/res/devel/templates/cpp-gui/main.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								Base/res/devel/templates/cpp-gui/main.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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(); | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								Base/res/devel/templates/cpp-library.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								Base/res/devel/templates/cpp-library.ini
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | [HackStudioTemplate] | ||||||
|  | Name=Shared Library (C++) | ||||||
|  | Description=Template for creating a C++ shared library. | ||||||
|  | IconName32x=cpp-library | ||||||
							
								
								
									
										49
									
								
								Base/res/devel/templates/cpp-library.postcreate
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								Base/res/devel/templates/cpp-library.postcreate
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
							
								
								
									
										5
									
								
								Base/res/devel/templates/empty.ini
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								Base/res/devel/templates/empty.ini
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | ||||||
|  | [HackStudioTemplate] | ||||||
|  | Name=Empty Project | ||||||
|  | Description=Template for creating an empty project with no files. | ||||||
|  | Priority=100 | ||||||
|  | IconName32x=empty | ||||||
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-basic.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-basic.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 511 B | 
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-gui.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-gui.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.5 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-library.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/cpp-library.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/empty.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Base/res/icons/hackstudio/templates-32x32/empty.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.9 KiB | 
|  | @ -62,6 +62,7 @@ chmod 4750 mnt/bin/keymap | ||||||
| chown 0:$utmp_gid mnt/bin/utmpupdate | chown 0:$utmp_gid mnt/bin/utmpupdate | ||||||
| chmod 2755 mnt/bin/utmpupdate | chmod 2755 mnt/bin/utmpupdate | ||||||
| chmod 600 mnt/etc/shadow | chmod 600 mnt/etc/shadow | ||||||
|  | chmod 755 mnt/res/devel/templates/*.postcreate | ||||||
| echo "done" | echo "done" | ||||||
| 
 | 
 | ||||||
| printf "creating initial filesystem structure... " | printf "creating initial filesystem structure... " | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
| add_subdirectory(LanguageServers) | add_subdirectory(LanguageServers) | ||||||
| add_subdirectory(LanguageClients) | add_subdirectory(LanguageClients) | ||||||
| 
 | 
 | ||||||
|  | compile_gml(Dialogs/NewProjectDialog.gml Dialogs/NewProjectDialogGML.h new_project_dialog_gml) | ||||||
|  | 
 | ||||||
| set(SOURCES | set(SOURCES | ||||||
|     CodeDocument.cpp |     CodeDocument.cpp | ||||||
|     CursorTool.cpp |     CursorTool.cpp | ||||||
|  | @ -11,6 +13,9 @@ set(SOURCES | ||||||
|     Debugger/DisassemblyWidget.cpp |     Debugger/DisassemblyWidget.cpp | ||||||
|     Debugger/RegistersModel.cpp |     Debugger/RegistersModel.cpp | ||||||
|     Debugger/VariablesModel.cpp |     Debugger/VariablesModel.cpp | ||||||
|  |     Dialogs/NewProjectDialog.cpp | ||||||
|  |     Dialogs/NewProjectDialogGML.h | ||||||
|  |     Dialogs/ProjectTemplatesModel.cpp | ||||||
|     Editor.cpp |     Editor.cpp | ||||||
|     EditorWrapper.cpp |     EditorWrapper.cpp | ||||||
|     FindInFilesWidget.cpp |     FindInFilesWidget.cpp | ||||||
|  | @ -26,6 +31,7 @@ set(SOURCES | ||||||
|     Locator.cpp |     Locator.cpp | ||||||
|     Project.cpp |     Project.cpp | ||||||
|     ProjectFile.cpp |     ProjectFile.cpp | ||||||
|  |     ProjectTemplate.cpp | ||||||
|     TerminalWrapper.cpp |     TerminalWrapper.cpp | ||||||
|     WidgetTool.cpp |     WidgetTool.cpp | ||||||
|     WidgetTreeModel.cpp |     WidgetTreeModel.cpp | ||||||
|  | @ -33,5 +39,5 @@ set(SOURCES | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| serenity_app(HackStudio ICON app-hack-studio) | 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) | add_dependencies(HackStudio CppLanguageServer) | ||||||
|  |  | ||||||
							
								
								
									
										244
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										244
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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())); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										115
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.gml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.gml
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										79
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								Userland/DevTools/HackStudio/Dialogs/NewProjectDialog.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										156
									
								
								Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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(); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										73
									
								
								Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								Userland/DevTools/HackStudio/Dialogs/ProjectTemplatesModel.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -31,6 +31,7 @@ | ||||||
| #include "Debugger/DebugInfoWidget.h" | #include "Debugger/DebugInfoWidget.h" | ||||||
| #include "Debugger/Debugger.h" | #include "Debugger/Debugger.h" | ||||||
| #include "Debugger/DisassemblyWidget.h" | #include "Debugger/DisassemblyWidget.h" | ||||||
|  | #include "Dialogs/NewProjectDialog.h" | ||||||
| #include "Editor.h" | #include "Editor.h" | ||||||
| #include "EditorWrapper.h" | #include "EditorWrapper.h" | ||||||
| #include "FindInFilesWidget.h" | #include "FindInFilesWidget.h" | ||||||
|  | @ -57,6 +58,7 @@ | ||||||
| #include <LibGUI/Application.h> | #include <LibGUI/Application.h> | ||||||
| #include <LibGUI/BoxLayout.h> | #include <LibGUI/BoxLayout.h> | ||||||
| #include <LibGUI/Button.h> | #include <LibGUI/Button.h> | ||||||
|  | #include <LibGUI/Dialog.h> | ||||||
| #include <LibGUI/EditingEngine.h> | #include <LibGUI/EditingEngine.h> | ||||||
| #include <LibGUI/FilePicker.h> | #include <LibGUI/FilePicker.h> | ||||||
| #include <LibGUI/InputBox.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_remove_current_editor_action = create_remove_current_editor_action(); | ||||||
|     m_open_action = create_open_action(); |     m_open_action = create_open_action(); | ||||||
|     m_save_action = create_save_action(); |     m_save_action = create_save_action(); | ||||||
|  |     m_new_project_action = create_new_project_action(); | ||||||
| 
 | 
 | ||||||
|     create_action_tab(*m_right_hand_splitter); |     create_action_tab(*m_right_hand_splitter); | ||||||
| 
 | 
 | ||||||
|  | @ -383,6 +386,18 @@ NonnullRefPtr<GUI::Action> HackStudioWidget::create_delete_action() | ||||||
|     return 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) | void HackStudioWidget::add_new_editor(GUI::Widget& parent) | ||||||
| { | { | ||||||
|     auto wrapper = EditorWrapper::construct(); |     auto wrapper = EditorWrapper::construct(); | ||||||
|  | @ -849,6 +864,7 @@ void HackStudioWidget::create_action_tab(GUI::Widget& parent) | ||||||
| void HackStudioWidget::create_app_menubar(GUI::MenuBar& menubar) | void HackStudioWidget::create_app_menubar(GUI::MenuBar& menubar) | ||||||
| { | { | ||||||
|     auto& app_menu = menubar.add_menu("Hack Studio"); |     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_open_action); | ||||||
|     app_menu.add_action(*m_save_action); |     app_menu.add_action(*m_save_action); | ||||||
|     app_menu.add_separator(); |     app_menu.add_separator(); | ||||||
|  |  | ||||||
|  | @ -84,6 +84,7 @@ private: | ||||||
|     NonnullRefPtr<GUI::Action> create_new_directory_action(); |     NonnullRefPtr<GUI::Action> create_new_directory_action(); | ||||||
|     NonnullRefPtr<GUI::Action> create_open_selected_action(); |     NonnullRefPtr<GUI::Action> create_open_selected_action(); | ||||||
|     NonnullRefPtr<GUI::Action> create_delete_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_next_editor_action(); | ||||||
|     NonnullRefPtr<GUI::Action> create_switch_to_previous_editor_action(); |     NonnullRefPtr<GUI::Action> create_switch_to_previous_editor_action(); | ||||||
|     NonnullRefPtr<GUI::Action> create_remove_current_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_new_directory_action; | ||||||
|     RefPtr<GUI::Action> m_open_selected_action; |     RefPtr<GUI::Action> m_open_selected_action; | ||||||
|     RefPtr<GUI::Action> m_delete_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_next_editor; | ||||||
|     RefPtr<GUI::Action> m_switch_to_previous_editor; |     RefPtr<GUI::Action> m_switch_to_previous_editor; | ||||||
|     RefPtr<GUI::Action> m_remove_current_editor_action; |     RefPtr<GUI::Action> m_remove_current_editor_action; | ||||||
|  |  | ||||||
							
								
								
									
										279
									
								
								Userland/DevTools/HackStudio/ProjectTemplate.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								Userland/DevTools/HackStudio/ProjectTemplate.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||||
|  | } | ||||||
							
								
								
									
										67
									
								
								Userland/DevTools/HackStudio/ProjectTemplate.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								Userland/DevTools/HackStudio/ProjectTemplate.h
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Nick Vella
						Nick Vella