diff options
| author | Ben Cho <ben.cho@qt.io> | 2024-11-12 11:41:59 +0100 |
|---|---|---|
| committer | Ben Cho <ben.cho@qt.io> | 2024-11-21 09:13:07 +0000 |
| commit | 911eba422f6b031a13e0c157e94d0ba43914e948 (patch) | |
| tree | f05c4a599755d3125e986b7c2506c0781cd9a004 | |
| parent | a9930e02fe053b4526c886caeda1eb41e8fec4cf (diff) | |
qt-cli: Initial implementation of qtcli tool
Implement basic features of `qtcli` in Go lang.
Support only a single command `new` for creating C++, Python classes.
Template files are sourced from Qt Creator and manually edited.
Task-number: VSCODEEXT-22
Change-Id: If7362654866e2712f35e59278e59232c940b73ce
Reviewed-by: Orkun Tokdemir <orkun.tokdemir@qt.io>
Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
26 files changed, 1631 insertions, 0 deletions
diff --git a/qt-cli/.gitignore b/qt-cli/.gitignore new file mode 100644 index 0000000..edf90b0 --- /dev/null +++ b/qt-cli/.gitignore @@ -0,0 +1 @@ +qtcli diff --git a/qt-cli/LICENSE b/qt-cli/LICENSE new file mode 100644 index 0000000..71ef8e6 --- /dev/null +++ b/qt-cli/LICENSE @@ -0,0 +1,177 @@ +Licensees holding valid commercial Qt licenses may use this software in +accordance with the the terms contained in a written agreement between +you and The Qt Company. Alternatively, the terms and conditions that were +accepted by the licensee when buying and/or downloading the +software do apply. + +For the latest licensing terms and conditions, see https://www.qt.io/terms-conditions. +For further information use the contact form at https://www.qt.io/contact-us. + +Alternatively, users may opt to utilize this software under the terms stipulated +in the following license agreement. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/qt-cli/README b/qt-cli/README new file mode 100644 index 0000000..183424d --- /dev/null +++ b/qt-cli/README @@ -0,0 +1,68 @@ +# Qt CLI (Command Line Interface) Tool + +A tool for creating Qt projects or files from the command line. + +## Build + +To build the project, ensure you have Go installed on your system. + +### Build the `qtcli` Binary + +```bash +$ go build . +``` + +## Usage + +Currently `new` command is prepared for creating C++ class as well as C++ source +and header files. +Below is the command-line output when you run qtcli without any arguments. + +```bash +$ ./qtcli +A CLI for creating Qt project and files + +Usage: + qtcli [flags] + qtcli [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + help Help about any command + new Create a new project or file(s) + +Flags: + -h, --help help for qtcli + -v, --version version for qtcli + +Use "qtcli [command] --help" for more information about a command. + +``` + +### How to create C++ class + +```bash +$ ./qtcli new class MyObject --type cpp --output-dir output +$ ls -lrt output + +total 8 +-rw-rw-r-- 1 bencho bencho 258 Nov 12 10:07 MyObject.h +-rw-rw-r-- 1 bencho bencho 192 Nov 12 10:07 MyObject.cpp +``` + +Additional flags can be used, for example: + +```bash +$ ./qtcli new class MyObject --type cpp --base QObject --include QObject --include QSharedData --include QDataStream --add Q_OBJECT --add QML_ELEMENT --qobject +``` +A full list of available flags can be found using the --help option. + +### How to create python class + +```bash +$ ./qtcli new class MyObject --type python --module PySide6 --import QWidget --output-dir output +``` + +## License + +This extension can be licensed under the Qt Commercial License and the LGPL 3.0. See the text of both licenses here. diff --git a/qt-cli/assets/assets.go b/qt-cli/assets/assets.go new file mode 100644 index 0000000..c744bd1 --- /dev/null +++ b/qt-cli/assets/assets.go @@ -0,0 +1,11 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package assets + +import ( + "embed" +) + +//go:embed * +var Assets embed.FS diff --git a/qt-cli/assets/templates/classes/cpp/config.yml b/qt-cli/assets/templates/classes/cpp/config.yml new file mode 100644 index 0000000..42896d7 --- /dev/null +++ b/qt-cli/assets/templates/classes/cpp/config.yml @@ -0,0 +1,32 @@ +version: "1" + +files: + - in: file.h.tmpl + out: '{{ .FileName }}' + fields: + - FileName: '{{ qEnsureExtension .ClassName ".h" }}' + - HeaderGuard: '{{ .FileName | cpp.CreateHeaderGuard }}' + - QtMacros: '{{ .qArgAdd }}' + - UseQtKeyword: true + - UsePragmaOnce: true + + - in: file.cpp.tmpl + out: '{{ .FileName }}' + fields: + - FileName: '{{ qEnsureExtension .ClassName ".cpp" }}' + +global: + fields: + - ClassName: '{{ .qArgName | cpp.ExtractClassName }}' + - BaseClass: '{{ .qArgBase }}' + - Includes: '{{ cpp.CreateIncludes .qArgInclude .qArgAdd }}' + - NamespaceOpenings: '{{ .qArgName | cpp.CreateNamespaceOpenings }}' + - NamespaceClosings: '{{ .qArgName | cpp.CreateNamespaceClosings }}' + - UseQSharedData: '{{ qContains .Includes "QSharedData" }}' + + header: | + {{ define "addLicense" }} + {{- if .qArgLicenseFile }} + {{- cpp.CreateLicense .qArgLicenseFile .ClassName .FileName }} + {{- end }} + {{ end }} diff --git a/qt-cli/assets/templates/classes/cpp/file.cpp.tmpl b/qt-cli/assets/templates/classes/cpp/file.cpp.tmpl new file mode 100644 index 0000000..0eea103 --- /dev/null +++ b/qt-cli/assets/templates/classes/cpp/file.cpp.tmpl @@ -0,0 +1,68 @@ +{{- template "addLicense" . }} + +{{- if .UseQSharedData }} +#include <utility> +{{- end }} + +{{ .NamespaceOpenings }} + +{{- if .UseQSharedData }} +class {{ .ClassName }}Data : public QSharedData +{ +public: + +}; +{{- end }} + +{{ define "ConstructorArgs" }} +{{- if .ConstructorParentClass }}{{ .ConstructorParentClass }} *parent{{ end -}} +{{ end }} + +{{ define "ConstructorInit" }} +{{- if or .BaseClass .UseQSharedData }} + : {{ if .BaseClass }}{{ .BaseClass }}{parent}{{ end -}} + {{- if and .BaseClass .UseQSharedData }}, {{ end -}} + {{- if .UseQSharedData }}data(new {{ .ClassName }}Data){{ end -}} +{{ end }} +{{ end }} + +{{ .ClassName }}::{{ .ClassName }}({{- template "ConstructorArgs" . }}) + {{- template "ConstructorInit" . }} +{ + +} + +{{- if .UseQSharedData }} +{{ .ClassName }}::{{ .ClassName }}(const {{ .ClassName }} &rhs) + : data{rhs.data} +{ + +} + +{{ .ClassName }}::{{ .ClassName }}({{ .ClassName }} &&rhs) + : data{std::move(rhs.data)} +{ + +} + +{{ .ClassName }} &{{ .ClassName }}::operator=(const {{ .ClassName }} &rhs) +{ + if (this != &rhs) + data = rhs.data; + return *this; +} + +{{ .ClassName }} &{{ .ClassName }}::operator=({{ .ClassName }} &&rhs) +{ + if (this != &rhs) + data = std::move(rhs.data); + return *this; +} + +{{ .ClassName }}::~{{ .ClassName }}() +{ + +} +{{- end }} + +{{ .NamespaceClosings }} diff --git a/qt-cli/assets/templates/classes/cpp/file.h.tmpl b/qt-cli/assets/templates/classes/cpp/file.h.tmpl new file mode 100644 index 0000000..1ea0e6b --- /dev/null +++ b/qt-cli/assets/templates/classes/cpp/file.h.tmpl @@ -0,0 +1,61 @@ +{{- template "addLicense" . }} +{{ if .UsePragmaOnce }} +#pragma once +{{ else }} +#ifndef {{ .HeaderGuard }} +#define {{ .HeaderGuard }} +{{ end }} + +{{ .NamespaceOpenings }} +{{ range (.Includes | qUnpack )}} +#include <{{ . }}> +{{- end }} + +{{- if .UseQSharedData }} +class {{ .ClassName }}Data; +{{- end }} + +{{ if .BaseClass }} +class {{ .ClassName }} : public {{ .BaseClass }} +{{- else }} +class {{ .ClassName }} +{{- end }} +{ +{{- range (.QtMacros | qUnpack) }} + {{ . }} +{{- end }} + +public: +{{- if .ConstructorParentClass }} + explicit {{ .ClassName }}({{ .ConstructorParentClass }} *parent = nullptr); +{{- else }} + {{ .ClassName }}(); +{{- end }} + +{{- if .UseQSharedData }} + {{ .ClassName }}(const {{ .ClassName }} &); + {{ .ClassName }}({{ .ClassName }} &&); + {{ .ClassName }} &operator=(const {{ .ClassName }} &); + {{ .ClassName }} &operator=({{ .ClassName }} &&); + ~{{ .ClassName }}(); +{{- end }} + +{{- if .IsQObject }} +{{- if .UseQtKeyword }} +signals: +{{- else }} +Q_SIGNALS: +{{- end }} +{{- end }} + +{{- if .UseQSharedData }} +private: + QSharedDataPointer<{{ .ClassName }}Data> data; +{{- end }} +}; + +{{ .NamespaceClosings }} + +{{- if not .UsePragmaOnce }} +#endif // {{ .HeaderGuard }} +{{- end }} diff --git a/qt-cli/assets/templates/classes/python/config.yml b/qt-cli/assets/templates/classes/python/config.yml new file mode 100644 index 0000000..2a3d99d --- /dev/null +++ b/qt-cli/assets/templates/classes/python/config.yml @@ -0,0 +1,12 @@ +version: "1" + +files: + - in: file.py.tmpl + out: '{{ .ClassName }}.py' + fields: + - ClassName: '{{ .qArgName }}' + BaseClass: '{{ .qArgBase }}' + Module: '{{ .qArgModule }}' + ImportQtCore: '{{ qContains .qArgImport "QtCore" }}' + ImportQtQuick: '{{ qContains .qArgImport "QtQuick" }}' + ImportQtWidgets: '{{ qContains .qArgImport "QtWidgets" }}' diff --git a/qt-cli/assets/templates/classes/python/file.py.tmpl b/qt-cli/assets/templates/classes/python/file.py.tmpl new file mode 100644 index 0000000..4737fdc --- /dev/null +++ b/qt-cli/assets/templates/classes/python/file.py.tmpl @@ -0,0 +1,22 @@ +# This Python file uses the following encoding: utf-8 +{{ if .Module }} + +{{- if .ImportQtCore }} +from {{ .Module }} import QtCore +{{- end }} +{{- if .ImportQtWidgets }} +from {{ .Module }} import QtWidgets +{{- end }} +{{- if .ImportQtQuick }} +from {{ .Module }} import QtQuick +{{- end }} + +{{- end }} +{{ if .BaseClass }} +class {{ .ClassName }}({{ .BaseClass }}): +{{- else }} +class {{ .ClassName }}: +{{- end }} + def __init__(self): + pass + diff --git a/qt-cli/assets/templates/licenses/cpp.tmpl b/qt-cli/assets/templates/licenses/cpp.tmpl new file mode 100644 index 0000000..9b0b118 --- /dev/null +++ b/qt-cli/assets/templates/licenses/cpp.tmpl @@ -0,0 +1,11 @@ +// Qt License Template Example <Edit this file as needed> +// +// Special Keywords: +// - Year = {{ .Year }}, Month = {{ .Month }}, Day = {{ .Day }}, +// - Date = {{ .Date }} +// - User = {{ .User }} +// - FileName = {{ .FileName }} +// - ClassName = {{ .ClassName }} +// +// Environment Variables: +// - call GetEnv: {{ "HOME" | qEnv }} diff --git a/qt-cli/cmd/new-class.go b/qt-cli/cmd/new-class.go new file mode 100644 index 0000000..7d6634d --- /dev/null +++ b/qt-cli/cmd/new-class.go @@ -0,0 +1,95 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package cmd + +import ( + "qtcli/generator" + "qtcli/util" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var classType string +var base string + +var cppMacroList []string +var cppIncludeList []string +var cppIsQObject bool + +var pythonModuleName string +var pythonImportList []string + +var newClassCmd = &cobra.Command{ + Use: "class <name> --type <type>", + Short: util.Msg("Create a new class"), + Run: func(cmd *cobra.Command, args []string) { + if len(args) < 1 { + cmd.Help() + return + } + + g := generator.NewGenerator(&generator.GeneratorInputData{ + Category: generator.TargetCategoryClass, + Type: classType, + Name: args[0], + OutputDir: outputDir, + LicenseFile: licenseTemplatePath, + CustomTemplateDir: customTemplateDir, + + CppBaseClass: base, + CppMacroList: cppMacroList, + CppIncludeList: cppIncludeList, + CppClassIsQObject: cppIsQObject, + CppUsePragma: true, + + PythonBaseClass: base, + PythonModuleName: pythonModuleName, + PythonImportList: pythonImportList, + }) + + _, err := g.Run() + if err != nil { + logrus.Fatal(err) + } + }, +} + +func init() { + // common + flags := newClassCmd.Flags() + flags.StringVarP( + &classType, "type", "t", "", + util.Msg("Specify file type to create")) + + flags.StringVarP( + &base, "base", "b", "", + util.Msg("Base class name")) + + newClassCmd.MarkFlagRequired("type") + + // cpp related + flags.StringSliceVarP( + &cppMacroList, "add", "a", []string{}, + util.Msg("Qt macro to add. (e.g., Q_OBJECT, QML_ELEMENT)")) + + flags.StringSliceVarP( + &cppIncludeList, "include", "i", []string{}, + util.Msg("Qt classe to include in a header file")) + + flags.BoolVarP( + &cppIsQObject, "qobject", "q", false, + util.Msg("Specify if class is a QObject-derived class")) + + // python related + flags.StringVarP( + &pythonModuleName, "module", "m", "PySide6", + util.Msg("Qt for Python Module")) + + flags.StringSliceVar( + &pythonImportList, "import", []string{}, + util.Msg("Qt classe to import")) + + newCmd.AddCommand(newClassCmd) +} diff --git a/qt-cli/cmd/new.go b/qt-cli/cmd/new.go new file mode 100644 index 0000000..7caf42d --- /dev/null +++ b/qt-cli/cmd/new.go @@ -0,0 +1,41 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package cmd + +import ( + "qtcli/prompt" + "qtcli/util" + + "github.com/spf13/cobra" +) + +var outputDir string +var customTemplateDir string +var licenseTemplatePath string + +var newCmd = &cobra.Command{ + Use: "new", + Short: util.Msg("Create a new project or file(s)"), + Run: func(cmd *cobra.Command, args []string) { + prompt.RunNew() + }, +} + +func init() { + flags := newCmd.PersistentFlags() + + flags.StringVarP( + &outputDir, "output-dir", "d", "", + util.Msg("Output directory")) + + flags.StringVarP( + &customTemplateDir, "template-dir", "p", "", + util.Msg("Specify a path to the custom template directory")) + + flags.StringVarP( + &licenseTemplatePath, "license-file", "l", "", + util.Msg("Specify a path to the license template file")) + + rootCmd.AddCommand(newCmd) +} diff --git a/qt-cli/cmd/root.go b/qt-cli/cmd/root.go new file mode 100644 index 0000000..b407fc6 --- /dev/null +++ b/qt-cli/cmd/root.go @@ -0,0 +1,41 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package cmd + +import ( + "os" + "qtcli/util" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var verbose bool + +var rootCmd = &cobra.Command{ + Use: "qtcli", + Short: util.Msg("A CLI for creating Qt project and files"), + Version: "0.1", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + if verbose { + logrus.SetLevel(logrus.DebugLevel) + } + }, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.PersistentFlags().BoolVarP( + &verbose, "verbose", "v", false, util.Msg("Enable verbose output")) + +} diff --git a/qt-cli/generator/config-yml.go b/qt-cli/generator/config-yml.go new file mode 100644 index 0000000..693362b --- /dev/null +++ b/qt-cli/generator/config-yml.go @@ -0,0 +1,67 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package generator + +import ( + "io/fs" + "qtcli/util" + + "gopkg.in/yaml.v3" +) + +// config file format related +type ConfigData struct { + Version string `yaml:"version"` + Files []ConfigEntryFile `yaml:"files"` + Global ConfigEntryGlobal `yaml:"global"` +} + +type ConfigEntryFile struct { + In string `yaml:"in"` + Out string `yaml:"out"` + FieldsList []ConfigEntryFields `yaml:"fields"` + When string `yaml:"when"` +} + +type ConfigEntryGlobal struct { + FieldsList []ConfigEntryFields `yaml:"fields"` + Header string `yaml:"header"` +} + +type ConfigEntryFields util.StringAnyMap + +func (g *ConfigEntryFields) expandBy( + expander *util.TemplateExpander, +) (util.StringAnyMap, error) { + all := util.StringAnyMap{} + + for name, expr := range *g { + if str, ok := expr.(string); ok { + expanded, err := expander.Name(name).RunString(str) + if err != nil { + return util.StringAnyMap{}, err + } + + all[name] = expanded + } else { + all[name] = expr + } + } + + return all, nil +} + +func readConfig(targetFS fs.FS, filePath string) (ConfigData, error) { + raw, err := util.ReadAllFromFS(targetFS, filePath) + if err != nil { + return ConfigData{}, err + } + + var config ConfigData + if err := yaml.Unmarshal(raw, &config); err != nil { + return ConfigData{}, err + } + + return config, nil +} diff --git a/qt-cli/generator/funcs-cpp.go b/qt-cli/generator/funcs-cpp.go new file mode 100644 index 0000000..85ea1cf --- /dev/null +++ b/qt-cli/generator/funcs-cpp.go @@ -0,0 +1,116 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package generator + +import ( + "fmt" + "qtcli/util" + "sort" + "strings" + "unicode" +) + +type CppFuncs struct{} + +func (cpp CppFuncs) ExtractClassName(fqcn string) string { + return extractClassNameOnly(fqcn) +} + +func (cpp CppFuncs) CreateNamespaceOpenings(fqcn string) string { + splits := strings.Split(fqcn, "::") + output := []string{} + + for i := 0; i < len(splits)-1; i++ { + output = append(output, fmt.Sprintf("namespace %s {", splits[i])) + } + + return strings.Join(output, "\n") +} + +func (cpp CppFuncs) CreateNamespaceClosings(fqcn string) string { + splits := strings.Split(fqcn, "::") + output := []string{} + + for i := 0; i < len(splits)-1; i++ { + output = append(output, fmt.Sprintf("} // namespace %s", splits[i])) + } + + return strings.Join(output, "\n") +} + +func (cpp CppFuncs) CreateHeaderGuard(fileName string) string { + return strings.ReplaceAll(strings.ToUpper(fileName), ".", "_") +} + +func (cpp CppFuncs) CreateIncludes( + includes []string, macros []string) []string { + all := []string{} + + sort.Strings(includes) + hasModuleName := true + + for _, name := range includes { + if !mightBeQtClass(name) { + continue + } + + item := name + + if hasModuleName { + module := findModuleName(name) + if module != "" { + item = module + "/" + name + } + } + + all = append(all, item) + } + + return all +} + +func (cpp CppFuncs) CreateLicense( + licenseTemplatePath string, + className string, + fileName string, +) string { + str, _ := generateLicense(licenseTemplatePath, util.StringAnyMap{ + "ClassName": className, + "FileName": fileName, + }) + + return str +} + +func mightBeQtClass(name string) bool { + return len(name) >= 2 && + name[0] == 'Q' && + unicode.IsUpper(rune(name[1])) +} + +func findModuleName(name string) string { + switch name { + case "QObject", "QSharedData": + return "QtCore" + + case "QWidget", "QMainWindow": + return "QtWidgets" + + case "QQuickItem": + return "QtQuick" + + case "QQmlEngine": + return "QtQml" + } + + return "" +} + +func extractClassNameOnly(fqcn string) string { + splits := strings.Split(fqcn, "::") + if len(splits) == 0 { + return "" + } + return splits[len(splits)-1] +} diff --git a/qt-cli/generator/funcs-general.go b/qt-cli/generator/funcs-general.go new file mode 100644 index 0000000..9203f15 --- /dev/null +++ b/qt-cli/generator/funcs-general.go @@ -0,0 +1,67 @@ +package generator + +import ( + "os" + "path/filepath" + "slices" + "strings" + "text/template" +) + +func createGeneralFuncMap() template.FuncMap { + return template.FuncMap{ + "qEnv": func(name string) string { + return os.Getenv(name) + }, + + "qJoin": func(items []string, sep string) string { + return strings.Join(items, sep) + }, + + "qContains": func(hayStack interface{}, needle string) string { + contained := false + switch t := hayStack.(type) { + case string: + if strings.Contains(t, needle) { + contained = true + } + + case []string: + contained = slices.Contains(t, needle) + } + + if contained { + return "true" + } else { + return "" + } + }, + + "qUnpack": func(input string) []string { + // [AAA BBB] -> []string{"AAA", "BBB"} + input = strings.TrimSpace(input) + if len(input) == 0 { + return []string{} + } + + if strings.HasPrefix(input, "[") && strings.HasSuffix(input, "]") { + if len(input) == 2 { + return []string{} + } + + return strings.Split(input[1:len(input)-1], " ") + } + + return []string{input} + }, + + "qEnsureExtension": func(filename string, ext string) string { + extracted := filepath.Ext(filename) + if len(extracted) != 0 { + return filename + } + + return filename + ext + }, + } +} diff --git a/qt-cli/generator/generator.go b/qt-cli/generator/generator.go new file mode 100644 index 0000000..e3bfc32 --- /dev/null +++ b/qt-cli/generator/generator.go @@ -0,0 +1,270 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package generator + +import ( + "fmt" + "io/fs" + "os" + "path/filepath" + "qtcli/assets" + "qtcli/util" + "strings" + "text/template" + + "github.com/sirupsen/logrus" +) + +type Generator struct { + GeneratorInputData + TypeConst TargetType + Config GeneratorConfig + GlobalContext GeneratorContext +} + +// note, +// this is a maximal set of possible input data +type GeneratorInputData struct { + Category TargetCategory + Type string + Name string + OutputDir string + LicenseFile string + CustomTemplateDir string + + CppBaseClass string + CppMacroList []string + CppIncludeList []string + CppClassIsQObject bool + CppUsePragma bool + + PythonBaseClass string + PythonModuleName string + PythonImportList []string +} + +type GeneratorConfig struct { + Contents ConfigData + BaseFS fs.FS + BaseDir string + FilePath string +} + +type GeneratorContext struct { + Data util.StringAnyMap + Funcs template.FuncMap + Header string +} + +type GeneratorResult struct { + FileNames []string +} + +func NewGenerator(input *GeneratorInputData) *Generator { + return &Generator{ + GeneratorInputData: *input, + } +} + +func (g *Generator) Run() (GeneratorResult, error) { + if err := g.validate(); err != nil { + return GeneratorResult{}, err + } + + if err := g.prepareContext(); err != nil { + return GeneratorResult{}, err + } + + generateFiles := []string{} + + for index, file := range g.Config.Contents.Files { + logrus.Debug(fmt.Sprintf( + "processing a file (%v/%v), in = %v", + index+1, len(g.Config.Contents.Files), file.In)) + + when, err := g.evalWhenCondition(file) + if err != nil { + return GeneratorResult{}, err + } + + if !when { + logrus.Debug( + "skipping generation ", + "because 'when' condition was not satisfied") + continue + } + + fileName, err := g.runSingleFile(file) + if err != nil { + return GeneratorResult{}, err + } + + generateFiles = append(generateFiles, fileName) + + } + + return GeneratorResult{FileNames: generateFiles}, nil +} + +func (g *Generator) evalWhenCondition(file ConfigEntryFile) (bool, error) { + if len(file.When) == 0 { + return true, nil + } + + out, err := util.NewTemplateExpander(). + Name(file.In). + Data(g.GlobalContext.Data). + Funcs(g.GlobalContext.Funcs). + RunString(file.When) + if err != nil { + return false, err + } + + if out != "true" { + return false, nil + } + + return true, nil +} + +func (g *Generator) validate() error { + logrus.Debug(fmt.Sprintf( + "validating input data, cat. = %v, type = %v, name = %v", + g.Category, g.Type, g.Name)) + + g.TypeConst = findNewTypeConst(g.Category, g.Type) + if g.TypeConst == TargetTypeInvalid { + return fmt.Errorf( + "invalid new type, given = '%v', '%v'", + g.Category, g.Type) + } + + g.Config.FilePath = findConfigPath(g.TypeConst) + if len(g.Config.FilePath) == 0 { + return fmt.Errorf( + "cannot determine a config file path, type = '%v'", + g.TypeConst) + } + + g.Config.BaseDir = filepath.Dir(g.Config.FilePath) + g.Config.BaseFS = assets.Assets + + if len(g.CustomTemplateDir) != 0 { + g.Config.BaseFS = os.DirFS(g.CustomTemplateDir) + } + + // load config.yml + logrus.Debug(fmt.Sprintf( + "reading config, file = '%v'", g.Config.FilePath)) + + config, err := readConfig(g.Config.BaseFS, g.Config.FilePath) + if err != nil { + return err + } + + g.Config.Contents = config + + return nil +} + +func (g *Generator) prepareContext() error { + logrus.Debug("preparing global context") + + // func + g.GlobalContext.Funcs = createGeneralFuncMap() + g.GlobalContext.Funcs["cpp"] = func() CppFuncs { + return CppFuncs{} + } + + // fields + logrus.Debug("processing fields") + expander := util.NewTemplateExpander().Funcs(g.GlobalContext.Funcs) + accumulatedFields := util.StringAnyMap{ + "qArgName": g.Name, + "qArgType": g.Type, + "qArgOutputDir": g.OutputDir, + "qArgLicenseFile": g.LicenseFile, + "qArgTemplateDir": g.CustomTemplateDir, + + "qArgBase": g.CppBaseClass, + "qArgAdd": g.CppMacroList, + "qArgInclude": g.CppIncludeList, + "qArgQObject": g.CppClassIsQObject, + "qArgModule": g.PythonModuleName, + "qArgImport": g.PythonImportList, + } + + for _, group := range g.Config.Contents.Global.FieldsList { + out, err := group.expandBy(expander.Data(accumulatedFields)) + if err != nil { + return err + } + + accumulatedFields.Merge(out) + logrus.Debug(fmt.Sprintf("expanding fields, %v", accumulatedFields)) + } + + g.GlobalContext.Data = accumulatedFields + logrus.Debug(fmt.Sprintf("processing fields, done, value = %v", accumulatedFields)) + + // others + g.GlobalContext.Header = g.Config.Contents.Global.Header + + return nil +} + +func (g *Generator) runSingleFile(file ConfigEntryFile) (string, error) { + // update fields + allFields := util.StringAnyMap{} + allFields.Merge(g.GlobalContext.Data) + expander := util.NewTemplateExpander().Funcs(g.GlobalContext.Funcs) + + for _, group := range file.FieldsList { + localFields, err := group.expandBy(expander.Data(allFields)) + if err != nil { + return "", err + } + + allFields.Merge(localFields) + } + + // expand output file name + outputFileName, err := expander. + Name(file.In). + Data(allFields). + RunString(file.Out) + if err != nil { + return "", err + } + + // expand input contents + path := filepath.Join(g.Config.BaseDir, file.In) + body, err := util.ReadAllFromFS(g.Config.BaseFS, path) + if err != nil { + return "", err + } + + output, err := expander. + Name(outputFileName). + RunString(g.GlobalContext.Header + string(body)) + if err != nil { + return "", err + } + + // remove spaces at beginning + output = strings.TrimLeft(output, " \t\r\n") + + // save or write to console + if len(g.OutputDir) != 0 { + destPath := filepath.Join(g.OutputDir, outputFileName) + _, err := util.WriteAll([]byte(output), destPath) + if err != nil { + return "", err + } + } else { + util.PrintlnWithName(output, outputFileName) + } + + return outputFileName, nil +} diff --git a/qt-cli/generator/input.go b/qt-cli/generator/input.go new file mode 100644 index 0000000..fc788af --- /dev/null +++ b/qt-cli/generator/input.go @@ -0,0 +1,63 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package generator + +import ( + "slices" + "strings" +) + +type TargetCategory string + +const ( + TargetCategoryInvalid TargetCategory = "TargetCategoryInvalid" + TargetCategoryProject TargetCategory = "TargetCategoryProject" + TargetCategoryClass TargetCategory = "TargetCategoryClass" + TargetCategoryFile TargetCategory = "TargetCategoryFile" +) + +type TargetType string + +const ( + TargetTypeInvalid TargetType = "TargetTypeInvalid" + TargetClassCpp TargetType = "TargetClassCpp" + TargetClassPython TargetType = "TargetClassPython" +) + +type SearchDict = map[TargetType][]string + +var typeNamesDict = map[TargetCategory]SearchDict{ + TargetCategoryClass: { + TargetClassCpp: {"cpp"}, + TargetClassPython: {"python"}, + }, +} + +func findNewTypeConst(category TargetCategory, key string) TargetType { + key = strings.ToLower(key) + dict := typeNamesDict[category] + if dict == nil { + return TargetTypeInvalid + } + + for typeId, possibleNames := range dict { + if slices.Contains(possibleNames, key) { + return typeId + } + } + + return TargetTypeInvalid +} + +func findConfigPath(newType TargetType) string { + switch newType { + case TargetClassCpp: + return "templates/classes/cpp/config.yml" + + case TargetClassPython: + return "templates/classes/python/config.yml" + } + + return "" +} diff --git a/qt-cli/generator/license.go b/qt-cli/generator/license.go new file mode 100644 index 0000000..7f9c26a --- /dev/null +++ b/qt-cli/generator/license.go @@ -0,0 +1,43 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package generator + +import ( + "os" + "os/user" + "path/filepath" + "qtcli/util" + "text/template" + "time" +) + +func generateLicense( + licenseTemplatePath string, + data util.StringAnyMap, +) (string, error) { + if len(licenseTemplatePath) == 0 { + return "", nil + } + + now := time.Now() + user, _ := user.Current() + dataAll := util.StringAnyMap{ + "Year": now.Format("2006"), + "Month": now.Format("01"), + "Day": now.Format("02"), + "Date": now.Format("2006-01-02"), + "User": user.Username, + } + dataAll.Merge(data) + + return util.NewTemplateExpander(). + Name(filepath.Base(licenseTemplatePath)). + Data(dataAll). + Funcs(template.FuncMap{ + "qEnv": func(name string) string { + return os.Getenv(name) + }, + }). + RunFile(licenseTemplatePath) +} diff --git a/qt-cli/go.mod b/qt-cli/go.mod new file mode 100644 index 0000000..14d0172 --- /dev/null +++ b/qt-cli/go.mod @@ -0,0 +1,17 @@ +module qtcli + +go 1.23.2 + +require ( + github.com/manifoldco/promptui v0.9.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.27.0 // indirect +) diff --git a/qt-cli/go.sum b/qt-cli/go.sum new file mode 100644 index 0000000..c4ac204 --- /dev/null +++ b/qt-cli/go.sum @@ -0,0 +1,35 @@ +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/qt-cli/main.go b/qt-cli/main.go new file mode 100644 index 0000000..536faf0 --- /dev/null +++ b/qt-cli/main.go @@ -0,0 +1,12 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package main + +import ( + "qtcli/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/qt-cli/prompt/prompt-class.go b/qt-cli/prompt/prompt-class.go new file mode 100644 index 0000000..65138e7 --- /dev/null +++ b/qt-cli/prompt/prompt-class.go @@ -0,0 +1,122 @@ +package prompt + +import ( + "qtcli/generator" + + "github.com/manifoldco/promptui" +) + +func runTypeSelection() (generator.TargetType, error) { + items := []struct { + Name string + Id generator.TargetType + }{ + {Name: "C++ Class", Id: generator.TargetClassCpp}, + {Name: "Python Class", Id: generator.TargetClassPython}, + } + + templates := &promptui.SelectTemplates{ + Selected: "{{ .Name }}", + Inactive: "\U00002002 {{ .Name }}", + Active: "\U00002192 {{ .Name | bold | underline }}", + } + + prompt := promptui.Select{ + Label: "What do you want to create?", + Items: items, + Templates: templates, + } + + index, _, err := prompt.Run() + if err != nil { + return generator.TargetTypeInvalid, err + } + + return items[index].Id, nil +} + +func RunNewCppClass() (generator.GeneratorInputData, error) { + fallback := generator.GeneratorInputData{} + promptName := promptui.Prompt{ + Label: "Class Name", + } + + promptBaseClass := promptui.Prompt{ + Label: "Base class", + } + + promptOutputDir := promptui.Prompt{ + Label: "Output Dir", + } + + className, err := promptName.Run() + if err != nil { + return fallback, err + } + + baseClass, err := promptBaseClass.Run() + if err != nil { + return fallback, err + } + + outputDir, err := promptOutputDir.Run() + if err != nil { + return fallback, err + } + + return generator.GeneratorInputData{ + Category: generator.TargetCategoryClass, + Type: "cpp", + Name: className, + OutputDir: outputDir, + CppBaseClass: baseClass, + }, nil +} + +func RunNewPythonClass() (generator.GeneratorInputData, error) { + fallback := generator.GeneratorInputData{} + promptModule := promptui.Prompt{ + Label: "Python Module (PySide6, PySide2, etc)", + } + + promptName := promptui.Prompt{ + Label: "Class Name (could include namespaces)", + } + + promptBaseClass := promptui.Prompt{ + Label: "Base class", + } + + promptOutputDir := promptui.Prompt{ + Label: "Output Dir", + } + + moduleName, err := promptModule.Run() + if err != nil { + return fallback, err + } + + className, err := promptName.Run() + if err != nil { + return fallback, err + } + + baseClass, err := promptBaseClass.Run() + if err != nil { + return fallback, err + } + + outputDir, err := promptOutputDir.Run() + if err != nil { + return fallback, err + } + + return generator.GeneratorInputData{ + Category: generator.TargetCategoryClass, + Type: "python", + Name: className, + OutputDir: outputDir, + PythonBaseClass: baseClass, + PythonModuleName: moduleName, + }, nil +} diff --git a/qt-cli/prompt/prompt.go b/qt-cli/prompt/prompt.go new file mode 100644 index 0000000..b06d5cd --- /dev/null +++ b/qt-cli/prompt/prompt.go @@ -0,0 +1,43 @@ +package prompt + +import ( + "fmt" + "qtcli/generator" + + "github.com/sirupsen/logrus" +) + +func RunNew() error { + newType, err := runTypeSelection() + if err != nil { + return err + } + + inputData, err := getInputData(newType) + if err != nil { + return nil + } + + g := generator.NewGenerator(&inputData) + _, err = g.Run() + if err != nil { + logrus.Fatal(err) + } + + return nil +} + +func getInputData(newType generator.TargetType) ( + generator.GeneratorInputData, error) { + switch newType { + case generator.TargetClassCpp: + return RunNewCppClass() + + case generator.TargetClassPython: + return RunNewPythonClass() + + default: + return generator.GeneratorInputData{}, + fmt.Errorf("not supported yet") + } +} diff --git a/qt-cli/util/expander.go b/qt-cli/util/expander.go new file mode 100644 index 0000000..ff3c4cd --- /dev/null +++ b/qt-cli/util/expander.go @@ -0,0 +1,70 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package util + +import ( + "bytes" + "io" + "text/template" +) + +type TemplateExpander struct { + data StringAnyMap + funcs template.FuncMap + name string +} + +func NewTemplateExpander() *TemplateExpander { + return &TemplateExpander{ + data: StringAnyMap{}, + funcs: template.FuncMap{}, + } +} + +func (e *TemplateExpander) Name(name string) *TemplateExpander { + e.name = name + return e +} + +func (e *TemplateExpander) Data(data StringAnyMap) *TemplateExpander { + e.data = data + return e +} + +func (e *TemplateExpander) Funcs(funcs template.FuncMap) *TemplateExpander { + e.funcs = funcs + return e +} + +func (e *TemplateExpander) RunString(templateString string) (string, error) { + return e.execTemplate(template. + New(e.name). + Funcs(e.funcs). + Parse(templateString)) +} + +func (e *TemplateExpander) RunFile(filePath string) (string, error) { + return e.execTemplate(template. + New(e.name). + Funcs(e.funcs). + ParseFiles(filePath)) +} + +func (e *TemplateExpander) execTemplate( + tmpl *template.Template, + err error, +) (string, error) { + if err != nil { + return "", err + } + + var buffer bytes.Buffer + var io io.Writer = &buffer + err = tmpl.Execute(io, e.data) + if err != nil { + return "", err + } + + return buffer.String(), nil +} diff --git a/qt-cli/util/util.go b/qt-cli/util/util.go new file mode 100644 index 0000000..a17db27 --- /dev/null +++ b/qt-cli/util/util.go @@ -0,0 +1,66 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only + +package util + +import ( + "fmt" + "io" + "io/fs" + "os" + "path/filepath" +) + +type StringAnyMap map[string]interface{} + +func (m *StringAnyMap) Merge(other StringAnyMap) { + for k, v := range other { + (*m)[k] = v + } +} + +func ReadAllFromFS(targetFS fs.FS, path string) ([]byte, error) { + stat, err := fs.Stat(targetFS, path) + if err != nil { + return []byte{}, + fmt.Errorf("cannot read file info, given %v", path) + } + + if !stat.Mode().IsRegular() { + return []byte{}, + fmt.Errorf("cannot read non-regular file, given = %v", path) + } + + file, err := targetFS.Open(path) + if err != nil { + return []byte{}, err + } + + defer file.Close() + return io.ReadAll(file) +} + +func WriteAll(data []byte, destPath string) (int, error) { + dir := filepath.Dir(destPath) + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return 0, err + } + + destFile, err := os.Create(destPath) + if err != nil { + return 0, err + } + + defer destFile.Close() + return destFile.Write(data) +} + +func PrintlnWithName(data string, fileName string) { + fmt.Println(">>>>>>>", fileName) + fmt.Print(data) + fmt.Println("<<<<<<<", fileName) +} + +func Msg(s string) string { + return s +} |
