~eliasnaur/gio-patches

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
2 2

[PATCH gio] cmd/gogio: generate boilerplate with init command

Details
Message ID
<20210621023911.64805-1-jackmordaunt@gmail.com>
DKIM signature
missing
Download raw message
Patch: +158 -0
'gogio init' generates simple Gio boilerplate into the current working directory.
It runs go module commands in order to get the sample app ready to hack on right-away.

Since gogio's scope is specialised to mobile builds, it wasn't clear how to
integrate arbitrary commands, such as the `init` command implemented in this patch.

Perhaps gogio's scope should loosen to be a more general purpose tool. In any
case, this patch makes the minimum changes to to implement the `init` command. 

Signed-off-by: Jack Mordaunt <jackmordaunt@gmail.com>
---
 cmd/gogio/help.go |   3 +
 cmd/gogio/init.go | 148 ++++++++++++++++++++++++++++++++++++++++++++++
 cmd/gogio/main.go |   7 +++
 3 files changed, 158 insertions(+)
 create mode 100644 cmd/gogio/init.go

diff --git a/cmd/gogio/help.go b/cmd/gogio/help.go
index ec9be8a..b5e0c85 100644
--- a/cmd/gogio/help.go
+++ b/cmd/gogio/help.go
@@ -11,6 +11,9 @@ Usage:
The gogio tool builds and packages Gio programs for platforms where additional
metadata or support files are required.

If gogio is run with the init command, it will generate a sample app in the 
current directory (ignoring all flags).

The package argument specifies an import path or a single Go source file to
package. Any run arguments are appended to os.Args at runtime.

diff --git a/cmd/gogio/init.go b/cmd/gogio/init.go
new file mode 100644
index 0000000..91b6e89
--- /dev/null
+++ b/cmd/gogio/init.go
@@ -0,0 +1,148 @@
// SPDX-License-Identifier: Unlicense OR MIT

package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

// initApp generates standard Gio boilerplate into a `main.go`.
func initApp(dir string) error {
	if dir == "" {
		cwd, err := os.Getwd()
		if err != nil {
			return fmt.Errorf("querying working directory: %w", err)
		}
		dir = cwd
	}
	if err := showCmd(exec.Command("go", "mod", "init", "app")); err != nil {
		return fmt.Errorf("initializing module: %w", err)
	}
	if err := ioutil.WriteFile(
		filepath.Join(dir, "main.go"),
		[]byte(boilerplate),
		0664,
	); err != nil {
		return fmt.Errorf("writing boilerplate: %w", err)
	}
	if err := showCmd(exec.Command("go", "mod", "tidy")); err != nil {
		return fmt.Errorf("tidying module: %w", err)
	}
	if err := showCmd(exec.Command("go", "run", ".")); err != nil {
		return fmt.Errorf("running module: %w", err)
	}
	return nil
}

// showCmd runs a command and renders the output to stdout.
func showCmd(cmd *exec.Cmd) error {
	out, err := runCmd(cmd)
	if err != nil {
		return fmt.Errorf("running command: %w", err)
	}
	if _, err := fmt.Print(out); err != nil {
		return fmt.Errorf("showing output: %w", err)
	}
	return nil
}

// boilerplate contains the sample program ready to hack on.
var boilerplate = strings.TrimSpace(`
package main

import (
	"log"
	"os"

	"gioui.org/app"
	"gioui.org/font/gofont"
	"gioui.org/io/system"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/unit"
	"gioui.org/widget"
	"gioui.org/widget/material"
)

func main() {
	w := app.NewWindow(
		app.Title("Hello Gio!"),
		app.Size(unit.Dp(800), unit.Dp(600)))
	go func() {
		if err := loop(w); err != nil {
			log.Printf("error: %v", err)
		}
		os.Exit(0)
	}()
	app.Main()
}

// loop executes the event loop. Itegrate with external systems here.
func loop(w *app.Window) error {
	var (
		ops op.Ops
		th  = material.NewTheme(gofont.Collection())
	)
	for event := range w.Events() {
		switch event := event.(type) {
		case system.DestroyEvent:
			return event.Err
		case system.FrameEvent:
			gtx := layout.NewContext(&ops, event)
			layoutUI(th, gtx)
			event.Frame(gtx.Ops)
		}
	}
	return nil
}

// Shorthand for commonly used types.
type (
	C = layout.Context
	D = layout.Dimensions
)

// State variables.
var (
	banner       string
	renameModule widget.Bool
	changeTitle  widget.Bool
	readDocs     widget.Bool
)

// layoutUI renders the UI into the context. Happy hacking!
func layoutUI(th *material.Theme, gtx C) D {
	if renameModule.Value && changeTitle.Value && readDocs.Value {
		banner = "Start Hacking!"
	} else {
		banner = "Hello Gio!"
	}
	return layout.Center.Layout(gtx, func(gtx C) D {
		return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(
			gtx,
			layout.Rigid(func(gtx C) D {
				return material.H1(th, banner).Layout(gtx)
			}),
			layout.Rigid(func(gtx C) D {
				return layout.Flex{Axis: layout.Vertical}.Layout(
					gtx,
					layout.Rigid(func(gtx C) D {
						return material.CheckBox(th, &renameModule, "rename module").Layout(gtx)
					}),
					layout.Rigid(func(gtx C) D {
						return material.CheckBox(th, &changeTitle, "change window title").Layout(gtx)
					}),
					layout.Rigid(func(gtx C) D {
						return material.CheckBox(th, &readDocs, "visit gioui.org/doc/architecture").Layout(gtx)
					}),
				)
			}),
		)
	})
}
`)
diff --git a/cmd/gogio/main.go b/cmd/gogio/main.go
index 38018f7..c16e4ec 100644
--- a/cmd/gogio/main.go
+++ b/cmd/gogio/main.go
@@ -44,6 +44,13 @@ func main() {
		fmt.Fprint(os.Stderr, mainUsage)
	}
	flag.Parse()
	if flag.Arg(0) == "init" {
		if err := initApp(flag.Arg(1)); err != nil {
			fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
			os.Exit(1)
		}
		os.Exit(0)
	}
	if err := flagValidate(); err != nil {
		fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
		os.Exit(1)
-- 
2.32.0

[gio/patches] build success

builds.sr.ht
Details
Message ID
<CC8YL5NR5LKZ.10XU9YLAVM4BV@cirno2>
In-Reply-To
<20210621023911.64805-1-jackmordaunt@gmail.com> (view parent)
DKIM signature
missing
Download raw message
gio/patches: SUCCESS in 20m12s

[cmd/gogio: generate boilerplate with init command][0] from [Jack Mordaunt][1]

[0]: https://lists.sr.ht/~eliasnaur/gio-patches/patches/23412
[1]: jackmordaunt@gmail.com

✓ #529888 SUCCESS gio/patches/apple.yml   https://builds.sr.ht/~eliasnaur/job/529888
✓ #529889 SUCCESS gio/patches/freebsd.yml https://builds.sr.ht/~eliasnaur/job/529889
✓ #529891 SUCCESS gio/patches/openbsd.yml https://builds.sr.ht/~eliasnaur/job/529891
✓ #529890 SUCCESS gio/patches/linux.yml   https://builds.sr.ht/~eliasnaur/job/529890
Details
Message ID
<CC95FSYN24VK.1Q5YZKI0U6KE8@testmac>
In-Reply-To
<20210621023911.64805-1-jackmordaunt@gmail.com> (view parent)
DKIM signature
fail
Download raw message
DKIM signature: fail
On Mon Jun 21, 2021 at 04:39 CEST, Jack Mordaunt wrote:
> 'gogio init' generates simple Gio boilerplate into the current working directory.
> It runs go module commands in order to get the sample app ready to hack on right-away.
>
> Since gogio's scope is specialised to mobile builds, it wasn't clear how to
> integrate arbitrary commands, such as the `init` command implemented in this patch.
>

Windows is also supported by the tool.

> Perhaps gogio's scope should loosen to be a more general purpose tool. In any
> case, this patch makes the minimum changes to to implement the `init` command. 
>
> Signed-off-by: Jack Mordaunt <jackmordaunt@gmail.com>
> ---
>  cmd/gogio/help.go |   3 +
>  cmd/gogio/init.go | 148 ++++++++++++++++++++++++++++++++++++++++++++++
>  cmd/gogio/main.go |   7 +++
>  3 files changed, 158 insertions(+)
>  create mode 100644 cmd/gogio/init.go
>
> diff --git a/cmd/gogio/help.go b/cmd/gogio/help.go
> index ec9be8a..b5e0c85 100644
> --- a/cmd/gogio/help.go
> +++ b/cmd/gogio/help.go
> @@ -11,6 +11,9 @@ Usage:
>  The gogio tool builds and packages Gio programs for platforms where additional
>  metadata or support files are required.
>  
> +If gogio is run with the init command, it will generate a sample app in the 
> +current directory (ignoring all flags).
> +
>  The package argument specifies an import path or a single Go source file to
>  package. Any run arguments are appended to os.Args at runtime.
>  
> diff --git a/cmd/gogio/init.go b/cmd/gogio/init.go
> new file mode 100644
> index 0000000..91b6e89
> --- /dev/null
> +++ b/cmd/gogio/init.go
> @@ -0,0 +1,148 @@
> +// SPDX-License-Identifier: Unlicense OR MIT
> +
> +package main
> +
> +import (
> +	"fmt"
> +	"io/ioutil"
> +	"os"
> +	"os/exec"
> +	"path/filepath"
> +	"strings"
> +)
> +
> +// initApp generates standard Gio boilerplate into a `main.go`.
> +func initApp(dir string) error {
> +	if dir == "" {
> +		cwd, err := os.Getwd()
> +		if err != nil {
> +			return fmt.Errorf("querying working directory: %w", err)
> +		}
> +		dir = cwd
> +	}
> +	if err := showCmd(exec.Command("go", "mod", "init", "app")); err != nil {
> +		return fmt.Errorf("initializing module: %w", err)
> +	}
> +	if err := ioutil.WriteFile(
> +		filepath.Join(dir, "main.go"),
> +		[]byte(boilerplate),
> +		0664,
> +	); err != nil {
> +		return fmt.Errorf("writing boilerplate: %w", err)
> +	}

Check that main.go, go.sum, go.mod don't already exist before writing
them.

> +	if err := showCmd(exec.Command("go", "mod", "tidy")); err != nil {
> +		return fmt.Errorf("tidying module: %w", err)
> +	}

This is a convenient way to fetch all required modules, but we
should select a gioui.org version that matches the sample. I suggest
using runtime/debug.ReadBuildInfo to extract the gioui.org module
version compiled into gogio.

> +	if err := showCmd(exec.Command("go", "run", ".")); err != nil {

Did you mean to run the sample? That isn't mentioned in the
documentation blurp and should probably be left to the user. You could
print a command line for them, however.

> +		return fmt.Errorf("running module: %w", err)
> +	}
> +	return nil
> +}
> +
> +// showCmd runs a command and renders the output to stdout.
> +func showCmd(cmd *exec.Cmd) error {
> +	out, err := runCmd(cmd)
> +	if err != nil {
> +		return fmt.Errorf("running command: %w", err)
> +	}
> +	if _, err := fmt.Print(out); err != nil {
> +		return fmt.Errorf("showing output: %w", err)
> +	}
> +	return nil
> +}
> +
> +// boilerplate contains the sample program ready to hack on.
> +var boilerplate = strings.TrimSpace(`
> +package main
> +
> +import (
> +	"log"
> +	"os"
> +
> +	"gioui.org/app"
> +	"gioui.org/font/gofont"
> +	"gioui.org/io/system"
> +	"gioui.org/layout"
> +	"gioui.org/op"
> +	"gioui.org/unit"
> +	"gioui.org/widget"
> +	"gioui.org/widget/material"
> +)
> +
> +func main() {
> +	w := app.NewWindow(
> +		app.Title("Hello Gio!"),
> +		app.Size(unit.Dp(800), unit.Dp(600)))
> +	go func() {
> +		if err := loop(w); err != nil {
> +			log.Printf("error: %v", err)
> +		}
> +		os.Exit(0)
> +	}()
> +	app.Main()
> +}
> +
> +// loop executes the event loop. Itegrate with external systems here.
> +func loop(w *app.Window) error {
> +	var (
> +		ops op.Ops
> +		th  = material.NewTheme(gofont.Collection())
> +	)
> +	for event := range w.Events() {
> +		switch event := event.(type) {
> +		case system.DestroyEvent:
> +			return event.Err
> +		case system.FrameEvent:
> +			gtx := layout.NewContext(&ops, event)
> +			layoutUI(th, gtx)
> +			event.Frame(gtx.Ops)
> +		}
> +	}
> +	return nil
> +}
> +
> +// Shorthand for commonly used types.
> +type (
> +	C = layout.Context
> +	D = layout.Dimensions
> +)
> +
> +// State variables.
> +var (
> +	banner       string
> +	renameModule widget.Bool
> +	changeTitle  widget.Bool
> +	readDocs     widget.Bool
> +)

If you're going to have state in the sample, it's better to (re-)introduce
your UI struct, with a single (l|L)ayout method.

> +
> +// layoutUI renders the UI into the context. Happy hacking!
> +func layoutUI(th *material.Theme, gtx C) D {
> +	if renameModule.Value && changeTitle.Value && readDocs.Value {
> +		banner = "Start Hacking!"
> +	} else {
> +		banner = "Hello Gio!"
> +	}
> +	return layout.Center.Layout(gtx, func(gtx C) D {
> +		return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(
> +			gtx,
> +			layout.Rigid(func(gtx C) D {
> +				return material.H1(th, banner).Layout(gtx)
> +			}),
> +			layout.Rigid(func(gtx C) D {
> +				return layout.Flex{Axis: layout.Vertical}.Layout(
> +					gtx,
> +					layout.Rigid(func(gtx C) D {
> +						return material.CheckBox(th, &renameModule, "rename module").Layout(gtx)
> +					}),
> +					layout.Rigid(func(gtx C) D {
> +						return material.CheckBox(th, &changeTitle, "change window title").Layout(gtx)
> +					}),
> +					layout.Rigid(func(gtx C) D {
> +						return material.CheckBox(th, &readDocs, "visit gioui.org/doc/architecture").Layout(gtx)
> +					}),
> +				)
> +			}),
> +		)
> +	})
> +}
> +`)

This should have a test that `gogio init`s and build the sample to keep
it from rotting.

> diff --git a/cmd/gogio/main.go b/cmd/gogio/main.go
> index 38018f7..c16e4ec 100644
> --- a/cmd/gogio/main.go
> +++ b/cmd/gogio/main.go
> @@ -44,6 +44,13 @@ func main() {
>  		fmt.Fprint(os.Stderr, mainUsage)
>  	}
>  	flag.Parse()
> +	if flag.Arg(0) == "init" {
> +		if err := initApp(flag.Arg(1)); err != nil {

The documentation says "current directory", but this seems to use a
directory from the command line. Leave it out?

> +			fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
> +			os.Exit(1)
> +		}
> +		os.Exit(0)
> +	}
>  	if err := flagValidate(); err != nil {
>  		fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
>  		os.Exit(1)
> -- 
> 2.32.0
Reply to thread Export thread (mbox)