sugar

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 15, 2026 License: MIT Imports: 6 Imported by: 0

README

sugar: Sweeten your Windows automation.

Warning: This project is currently in the Alpha stage (v0.x.x). APIs are subject to change and breaking changes may occur until the v1.0.0 release.

sugar is a flexible and safe Go library for Component Object Model (COM) automation on Windows. Built on top of the powerful go-ole library, it introduces Immutability and the Arena (Context) pattern to help you write clean code without worrying about resource leaks.

Key Features

  • Standard Execution Pattern (Do/Go): Automatically handles thread locking (LockOSThread) and COM initialization (CoInitialize).
  • Immutable Chain: All operations (Get, Call, etc.) return a new Chain (Interface) instance, preventing side effects on original objects.
  • Automatic Resource Management (Arena): All COM objects created within a context are automatically released in reverse order when the block completes.
  • Standard context.Context Integration: Leverage Go's standard context features for cancellation, timeouts, and value passing.
  • Expression-Based Automation: Navigate complex object hierarchies using a single string expression.
  • Application Specific Subpackages: Use type-safe wrappers for popular applications like Excel.

Installation

go get -u github.com/xll-gen/sugar

Quick Start (Generic)

A simple example using sugar.Do to launch Excel. Resource cleanup is handled automatically.

package main

import (
	"log"
	"github.com/xll-gen/sugar"
)

func main() {
	// sugar.Do guarantees COM initialization and automatic resource cleanup.
	err := sugar.Do(func(ctx sugar.Context) error {
		excel := ctx.Create("Excel.Application")
		if err := excel.Err(); err != nil {
			return err
		}
		
		// Schedule Excel to quit
		defer excel.Call("Quit")

		// Method chaining (Immutable pattern)
		excel.Put("Visible", true).
			Get("Workbooks").
			Call("Add")
            
		return nil
	})

	if err != nil {
		log.Fatalf("Automation failed: %v", err)
	}
}

Excel Subpackage (Type-Safe)

For common applications, sugar provides subpackages with friendly methods.

import "github.com/xll-gen/sugar/excel"

sugar.Do(func(ctx sugar.Context) error {
    app := excel.NewApplication(ctx)
    defer app.Quit()

    app.Put("Visible", true)
    
    wb := app.Workbooks().Add()
    sheet := wb.ActiveSheet()
    
    // Type-safe Range manipulation
    sheet.Range("A1").SetValue("Hello from Sugar!")
    
    return nil
})

Core Concepts

1. Standard Execution (sugar.Do & sugar.Go)

COM is sensitive to the execution thread. sugar provides safe entry points to manage this.

  • sugar.Do: Locks the current goroutine to an OS thread and executes synchronously.
  • sugar.Go: Starts a new goroutine (new OS thread) and independently initializes the COM environment for asynchronous work.
2. Immutable Chain

Methods like Get, Call, and ForEach always return a NEW Chain instance. Chain is now an interface, allowing for custom wrappers like the excel package.

workbooks := excel.Get("Workbooks") // 'excel' still points to Application
wb := workbooks.Call("Add")         // 'workbooks' still points to the Workbooks collection
3. Iteration with ForEach

You can iterate over COM collections using the ForEach method. Each item is provided as a Chain instance. Returning sugar.ErrForEachBreak stops the iteration and the error is recorded in the Chain.

sugar.Do(func(ctx sugar.Context) error {
    excel := ctx.Create("Excel.Application")
    workbooks := excel.Get("Workbooks")

    // Iterate through all open workbooks
    err := workbooks.ForEach(func(wb sugar.Chain) error {
        name, _ := wb.Get("Name").Value()
        fmt.Printf("Workbook: %v\n", name)
        
        // Stop after first item if needed
        return sugar.ErrForEachBreak
    }).Err()

    if errors.Is(err, sugar.ErrForEachBreak) {
        // Handled break
    }
    return nil
})
4. Arena Context

The sugar.Context acts as a resource collector (Arena). Any object created via ctx.Create, ctx.From, or derived from a chain is automatically registered with that context and cleaned up when the Do block ends.

Manual Release() calls are no longer necessary.

5. Nested Scopes

Use ctx.Do to create a nested arena for early resource cleanup.

sugar.Do(func(ctx sugar.Context) error {
    excel := ctx.Create("Excel.Application")
    
    ctx.Do(func(innerCtx sugar.Context) error {
        // Objects created in this block are released immediately when it ends.
        wb := excel.Get("Workbooks").Call("Add")
        return nil
    }) 
    // 'wb' is released here, while 'excel' remains valid.
    return nil
})

Expression-Based Automation (Subpackage)

The expression package allows you to manipulate complex hierarchies with a single line of code.

import "github.com/xll-gen/sugar/expression"

sugar.Do(func(ctx sugar.Context) error {
    excel := ctx.Create("Excel.Application")
    
    // Set complex paths at once
    expression.Put(excel, "ActiveSheet.Range('A1').Value", "Hello Sugar!")
    
    // Read values
    val, _ := expression.Get(excel, "ActiveSheet.Range('A1').Value")
    fmt.Println(val)
    return nil
})

Considerations

  • Windows Only: This library depends on Windows COM technology and only works on Windows OS.
  • Object Sharing Between Threads: Sharing raw IDispatch pointers between threads (goroutines) without proper marshaling is dangerous. We recommend creating independent objects in each goroutine using sugar.Go.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Documentation

Rendered for windows/amd64

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrForEachBreak is used to break out of a ForEach loop.
	ErrForEachBreak error = &ForEachBreak{}
)

Functions

func Do added in v0.2.0

func Do(fn func(ctx Context) error) error

Do executes the function with a Background context.

Example
package main

import (
	"fmt"
	"log"

	"github.com/xll-gen/sugar"
)

func main() {
	sugar.Do(func(ctx sugar.Context) error {
		excel := ctx.Create("Excel.Application")
		if err := excel.Err(); err != nil {
			log.Println("Failed to create Excel:", err)
			return err
		}
		defer excel.Call("Quit")

		excel.Put("Visible", true)
		wb := excel.Get("Workbooks").Call("Add")
		name, _ := wb.Get("Name").Value()
		fmt.Printf("Workbook Name: %v\n", name)
		return nil
	})
}

func Go added in v0.2.0

func Go(fn func(ctx Context) error)

Go executes the function in a new goroutine with a Background context.

Types

type Chain

type Chain interface {
	// Get retrieves a property from the current COM object and returns a NEW Chain
	// representing the property value. If the property is a COM object, it will
	// be automatically tracked if a Context is present.
	Get(prop string, params ...interface{}) Chain

	// Call executes a method on the current COM object and returns a NEW Chain
	// representing the return value. If the value is a COM object, it will
	// be automatically tracked if a Context is present.
	Call(method string, params ...interface{}) Chain

	// Put sets a property on the current COM object. It returns the same Chain
	// instance (or an error-carrying Chain) to allow further operations.
	Put(prop string, params ...interface{}) Chain

	// ForEach iterates over a COM collection (any object that implements IEnumVARIANT).
	// For each item, the callback is executed with a new Chain instance.
	//
	// To stop iteration:
	//   - Return nil to continue to the next item.
	//   - Return ErrForEachBreak (or an error wrapping it) to stop iteration.
	//   - Return any other error to stop and propagate the error to the parent Chain.
	//
	// NOTE: The break error is recorded in the Chain and should be checked manually
	// by the caller via Err() if they need to distinguish it from other errors.
	ForEach(callback func(item Chain) error) Chain

	// Fork creates a new independent reference to the current COM object.
	// Both the original and the forked Chain will point to the same object
	// but are managed as separate entries in the Context's arena.
	Fork() Chain

	// Store increases the reference count and returns the raw *ole.IDispatch.
	// The caller is responsible for calling Release() on the returned object
	// if it's not managed by sugar.Context.
	Store() (*ole.IDispatch, error)

	// Release manually releases the held COM object. Usually, this is handled
	// automatically by the sugar.Context, but can be used for early cleanup.
	Release() error

	// IsDispatch returns true if the last operation's result is a COM object (IDispatch).
	IsDispatch() bool

	// Value retrieves the underlying Go value of the last operation's result.
	// Returns an error if the result is a COM object (use Store() instead).
	Value() (interface{}, error)

	// Err returns the first error encountered in the chain of operations.
	Err() error
}

Chain provides a fluent interface for chaining OLE operations. It handles error propagation, allowing you to call multiple methods and check the error once at the end via Err().

func Create

func Create(progID string) Chain

Create starts a new chain by creating a new COM object from the given ProgID.

func From

func From(disp *ole.IDispatch) Chain

From starts a new chain with the given IDispatch.

func GetActive

func GetActive(progID string) Chain

GetActive starts a new chain by attaching to a running COM object.

Example
package main

import (
	"fmt"

	"github.com/xll-gen/sugar"
)

func main() {
	sugar.Do(func(ctx sugar.Context) error {
		excel := ctx.GetActive("Excel.Application")
		if err := excel.Err(); err != nil {
			fmt.Println("GetActive failed as expected.")
		} else {
			fmt.Println("GetActive succeeded.")
		}
		return nil
	})
}

type Context added in v0.2.0

type Context interface {
	context.Context
	// Track registers a Chain with the Context for automatic release.
	Track(ch Chain) Chain
	// Create is a wrapper around sugar.Create that automatically tracks the chain.
	Create(progID string) Chain
	// GetActive is a wrapper around sugar.GetActive that automatically tracks the chain.
	GetActive(progID string) Chain
	// From is a wrapper around sugar.From that automatically tracks the chain.
	From(disp *ole.IDispatch) Chain
	// Release releases all tracked chains in LIFO order.
	Release() error
	// Do executes the function within a nested scope of this context.
	Do(fn func(ctx Context) error) error
	// Go executes the function in a new goroutine branching from this context.
	Go(fn func(ctx Context) error)
}

Context manages the lifecycle of multiple Chains and implements context.Context.

func NewContext added in v0.2.0

func NewContext(parent context.Context) Context

NewContext creates a new Context with the given parent.

type ForEachBreak added in v0.4.2

type ForEachBreak struct {
	Value interface{}
}

ForEachBreak is returned when ForEach iteration is explicitly broken.

func (*ForEachBreak) Error added in v0.4.2

func (e *ForEachBreak) Error() string

func (*ForEachBreak) Is added in v0.4.2

func (e *ForEachBreak) Is(target error) bool

type Runner added in v0.2.0

type Runner struct {
	// contains filtered or unexported fields
}

Runner configures the execution environment for COM operations.

func With added in v0.2.0

func With(ctx context.Context) *Runner

With returns a new Runner with the specified parent context.

func (*Runner) Do added in v0.2.0

func (r *Runner) Do(fn func(ctx Context) error) (err error)

Do executes the provided function in the current goroutine.

func (*Runner) Go added in v0.2.0

func (r *Runner) Go(fn func(ctx Context) error)

Go executes the provided function in a new goroutine.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL