aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBen Cho <ben.cho@qt.io>2024-11-12 11:41:59 +0100
committerBen Cho <ben.cho@qt.io>2024-11-21 09:13:07 +0000
commit911eba422f6b031a13e0c157e94d0ba43914e948 (patch)
treef05c4a599755d3125e986b7c2506c0781cd9a004
parenta9930e02fe053b4526c886caeda1eb41e8fec4cf (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>
-rw-r--r--qt-cli/.gitignore1
-rw-r--r--qt-cli/LICENSE177
-rw-r--r--qt-cli/README68
-rw-r--r--qt-cli/assets/assets.go11
-rw-r--r--qt-cli/assets/templates/classes/cpp/config.yml32
-rw-r--r--qt-cli/assets/templates/classes/cpp/file.cpp.tmpl68
-rw-r--r--qt-cli/assets/templates/classes/cpp/file.h.tmpl61
-rw-r--r--qt-cli/assets/templates/classes/python/config.yml12
-rw-r--r--qt-cli/assets/templates/classes/python/file.py.tmpl22
-rw-r--r--qt-cli/assets/templates/licenses/cpp.tmpl11
-rw-r--r--qt-cli/cmd/new-class.go95
-rw-r--r--qt-cli/cmd/new.go41
-rw-r--r--qt-cli/cmd/root.go41
-rw-r--r--qt-cli/generator/config-yml.go67
-rw-r--r--qt-cli/generator/funcs-cpp.go116
-rw-r--r--qt-cli/generator/funcs-general.go67
-rw-r--r--qt-cli/generator/generator.go270
-rw-r--r--qt-cli/generator/input.go63
-rw-r--r--qt-cli/generator/license.go43
-rw-r--r--qt-cli/go.mod17
-rw-r--r--qt-cli/go.sum35
-rw-r--r--qt-cli/main.go12
-rw-r--r--qt-cli/prompt/prompt-class.go122
-rw-r--r--qt-cli/prompt/prompt.go43
-rw-r--r--qt-cli/util/expander.go70
-rw-r--r--qt-cli/util/util.go66
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
+}