From 967b77c51c6b3bdb3148275b54965d9aba183530 Mon Sep 17 00:00:00 2001 From: grngxd <36968271+grngxd@users.noreply.github.com> Date: Mon, 16 Jun 2025 22:58:47 +0100 Subject: [PATCH] init tiramisu --- .air.toml | 52 ++++++++++++++++++++ .gitignore | 2 + README.md | 4 ++ example/main.go | 49 +++++++++++++++++++ go.mod | 5 ++ go.sum | 2 + runtime/index.d.ts | 11 +++++ runtime/preload.ts | 5 ++ runtime/tsconfig.json | 12 +++++ tiramisu.go | 111 ++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 253 insertions(+) create mode 100644 .air.toml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 example/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 runtime/index.d.ts create mode 100644 runtime/preload.ts create mode 100644 runtime/tsconfig.json create mode 100644 tiramisu.go diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..d7ff5c6 --- /dev/null +++ b/.air.toml @@ -0,0 +1,52 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "tmp\\main.exe" + cmd = "go build -o ./tmp/main.exe ./example" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "runtime/out", "node_modules", "dist"] + exclude_file = [] + exclude_regex = ["_test.go", "out"] + exclude_unchanged = true + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html", "js", "ts"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = ["bunx tsc -p ./runtime"] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + silent = false + time = false + +[misc] + clean_on_exit = false + +[proxy] + app_port = 0 + enabled = false + proxy_port = 0 + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72c806f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +tmp/ +out/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c2634e6 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# 🍥 tiramisu +> ` is tiramisu a cake or a pie?` + +Build modern, cross-platform desktop apps in HTML + Go from one codebase. \ No newline at end of file diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..9177b22 --- /dev/null +++ b/example/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + + t "github.com/grngxd/tiramisu" + webview "github.com/webview/webview_go" +) + +func main() { + app := t.New(t.TiramisuOptions{ + Debug: true, + Width: 1200, + Height: 800, + Title: "Tiramisu Example", + Hints: webview.HintFixed, + }) + + app.Run(func() { + app.Bind("hello", func(args ...any) (any, error) { + if len(args) == 0 { + return "Hello, World!", nil + } + if len(args) == 1 { + return fmt.Sprintf("Hello, %s!", args[0]), nil + } + return "Hello, unknown!", nil + }) + + app.HTML(` + + + +

Tiramisu Example

+

Click the button to see a greeting:

+ + + + + + `) + }) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dbfc40b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/grngxd/tiramisu + +go 1.23.4 + +require github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2421e19 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0= +github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk= diff --git a/runtime/index.d.ts b/runtime/index.d.ts new file mode 100644 index 0000000..afb0fc3 --- /dev/null +++ b/runtime/index.d.ts @@ -0,0 +1,11 @@ +declare global { + interface Window { + invoke: (name: string, ...args: any[]) => Promise; + tiramisu: { + invoke: (name: string, ...args: any[]) => Promise; + }; + } +} + +export { }; + diff --git a/runtime/preload.ts b/runtime/preload.ts new file mode 100644 index 0000000..99e6691 --- /dev/null +++ b/runtime/preload.ts @@ -0,0 +1,5 @@ +const tiramisu = { + invoke: window.invoke +} + +window.tiramisu = tiramisu \ No newline at end of file diff --git a/runtime/tsconfig.json b/runtime/tsconfig.json new file mode 100644 index 0000000..f4772a2 --- /dev/null +++ b/runtime/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "node", + "strict": false, + "declaration": true, + "outDir": "./out", + "esModuleInterop": true, + "skipLibCheck": true + }, +} \ No newline at end of file diff --git a/tiramisu.go b/tiramisu.go new file mode 100644 index 0000000..8b42e76 --- /dev/null +++ b/tiramisu.go @@ -0,0 +1,111 @@ +package tiramisu + +import ( + "embed" + "fmt" + + wv "github.com/webview/webview_go" +) + +type TiramisuOptions struct { + Debug bool + Width int + Height int + Title string + Hints wv.Hint +} + +type FuncHandler func(args ...any) (any, error) + +type Tiramisu struct { + w wv.WebView + funcs map[string]FuncHandler +} + +func New(o TiramisuOptions) *Tiramisu { + w := wv.New(o.Debug) + t := &Tiramisu{ + w: w, + funcs: make(map[string]FuncHandler), + } + + w.SetSize(o.Width, o.Height, o.Hints) + w.SetTitle(o.Title) + + return t +} + +func (t *Tiramisu) Run(fn func()) { + defer t.w.Destroy() + t.w.Dispatch(func() { + t.injectJS() + + if fn != nil { + fn() + } + }) + t.w.Run() +} + +func (t *Tiramisu) bind(name string, fn FuncHandler) { + t.w.Bind(name, fn) +} + +func (t *Tiramisu) Bind(name string, fn FuncHandler) { + if _, exists := t.funcs[name]; exists { + panic(fmt.Sprintf("function %s is already bound", name)) + } + + t.funcs[name] = fn + t.bind(name, func(args ...any) (any, error) { + return t.invoke(name, args...) + }) +} + +func (t *Tiramisu) invoke(name string, args ...any) (any, error) { + fn, ok := t.funcs[name] + if !ok { + return nil, fmt.Errorf("function %s not found", name) + } + result, err := fn(args...) + if err != nil { + return nil, fmt.Errorf("error invoking function %s: %w", name, err) + } + return result, nil +} + +func (t *Tiramisu) Eval(js string) { + t.w.Eval(js) +} + +func (t *Tiramisu) Evalf(js string, args ...any) { + fmt := fmt.Sprintf(js, args...) + t.w.Eval(fmt) +} + +func (t *Tiramisu) HTML(html string) { + t.w.SetHtml(html) + t.injectJS() +} + +//go:embed runtime/out/* +var runtimeFS embed.FS + +func (t *Tiramisu) injectJS() { + js, err := runtimeFS.ReadFile("runtime/out/preload.js") + if err != nil { + panic(fmt.Sprintf("failed to read preload.js: %v", err)) + } + + t.w.Eval(string(js)) + t.bind("invoke", func(args ...any) (any, error) { + name, ok := args[0].(string) + if !ok { + return nil, fmt.Errorf("first argument must be a string, got %T", args[0]) + } + if len(args) == 1 { + return t.invoke(name) + } + return t.invoke(name, args[1:]...) + }) +}