From ad7fb4cd99887a4db5d3f5583cfbe0350bc55e79 Mon Sep 17 00:00:00 2001 From: Drew Smirnoff Date: Thu, 19 Mar 2026 13:01:39 +0400 Subject: [PATCH] feat: add windows support (#320) --- .github/workflows/ci.yml | 8 +++++- .goreleaser.yml | 5 ++++ docs/docs/installation.md | 19 ++++++++++++- main.go | 57 +++++++++++++++++++++++++++++++++++---- view/html.go | 30 ++------------------- view/terminal_unix.go | 32 ++++++++++++++++++++++ view/terminal_windows.go | 9 +++++++ 7 files changed, 125 insertions(+), 35 deletions(-) create mode 100644 view/terminal_unix.go create mode 100644 view/terminal_windows.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0824e836465d9ed8e925d1d795a7ea49412a183..249d379dff8f8c56f80cba69176ef93dd25140f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,10 @@ on: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] steps: - uses: actions/checkout@v6 @@ -22,3 +25,6 @@ jobs: - name: Test run: go test -v ./... + + - name: Build + run: go build -v ./... diff --git a/.goreleaser.yml b/.goreleaser.yml index 999f6eb25078ef6bb2c8905493ddd38ebe9e8100..d51a87b29efab929c16550dbb8656d33c3972b70 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -20,6 +20,7 @@ builds: goos: - linux - darwin # macOS + - windows goarch: - amd64 # Intel - arm64 # Apple Silicon @@ -39,10 +40,14 @@ archives: - # The archive configuration. # This will create .tar.gz files. formats: [tar.gz] + # A template for the archive file names. # e.g., matcha_1.2.3_darwin_amd64.tar.gz name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" # Files to include in the archive. + format_overrides: + - goos: windows + formats: ["zip"] files: - LICENSE diff --git a/docs/docs/installation.md b/docs/docs/installation.md index 076f20bbccdd57c9171c4fa93081c516c91266a3..4697e00393178d14ccee3c093ef158cd8290e63b 100644 --- a/docs/docs/installation.md +++ b/docs/docs/installation.md @@ -36,7 +36,9 @@ You can download pre-compiled binaries from the [Releases page](https://github.c 3. Move the binary to your path: ```bash mv matcha /usr/local/bin/ + chmod +x /usr/local/bin/executable_name ``` + 4. Run it: ```bash matcha @@ -100,7 +102,22 @@ You can download pre-compiled binaries from the [Releases page](https://github.c ## 🪟 Windows -Currently, there is no native support for Windows. Please see issue [#123](https://github.com/floatpane/matcha/issues/123) for more details. +### Manual Binary Download + +You can download pre-compiled binaries from the [Releases page](https://github.com/floatpane/matcha/releases). + +1. Move the executable to a permanent, dedicated location on your computer (e.g., C:\CLI Tools\MyTool\). +2. Open the System Properties window by searching for "environment variables" in the Windows Start menu and selecting "Edit the system environment variables". +3. Click the "Environment Variables..." button at the bottom of the System Properties window, under the "Advanced" tab. +4. Locate the "Path" variable in the "User variables for [Your Username]" section (for access only by your user account) or the "System variables" section (for all users). +5. Double-click on the "Path" variable to edit it. +6. Add the path to your executable's folder: + + In the "Edit environment variable" window, click "New". + Type or paste the full path to the folder where your executable is located (e.g., C:\CLI Tools\MyTool\). + Click "OK" on all open windows to save the changes. + +> Matcha will be added to WinGet as soons as possible! ### WSL diff --git a/main.go b/main.go index 086d8164f37bd4d1fcdca4519451a2770e4cf249..e8e5e3c703fc1b204ad2ea83c38b797848ed0b2d 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "archive/tar" + "archive/zip" "bytes" "compress/gzip" "encoding/base64" @@ -1954,7 +1955,13 @@ func runUpdateCLI() error { return fmt.Errorf("could not write asset to disk: %w", err) } - // If it's a tar.gz, extract and find the `matcha` binary + // Determine the expected binary name based on the OS. + binaryName := "matcha" + if runtime.GOOS == "windows" { + binaryName = "matcha.exe" + } + + // Extract the binary from the archive. var binPath string if strings.HasSuffix(assetName, ".tar.gz") || strings.HasSuffix(assetName, ".tgz") { f, err := os.Open(assetPath) @@ -1976,9 +1983,8 @@ func runUpdateCLI() error { return fmt.Errorf("error reading tar: %w", err) } name := filepath.Base(hdr.Name) - if name == "matcha" || strings.Contains(strings.ToLower(name), "matcha") && (hdr.Typeflag == tar.TypeReg) { - // write out the file - binPath = filepath.Join(tmpDir, "matcha") + if name == binaryName || strings.Contains(strings.ToLower(name), "matcha") && (hdr.Typeflag == tar.TypeReg) { + binPath = filepath.Join(tmpDir, binaryName) out, err := os.Create(binPath) if err != nil { return fmt.Errorf("could not create binary file: %w", err) @@ -1994,6 +2000,38 @@ func runUpdateCLI() error { break } } + } else if strings.HasSuffix(assetName, ".zip") { + zr, err := zip.OpenReader(assetPath) + if err != nil { + return fmt.Errorf("could not open zip archive: %w", err) + } + defer zr.Close() + for _, zf := range zr.File { + name := filepath.Base(zf.Name) + if name == binaryName || strings.Contains(strings.ToLower(name), "matcha") && !zf.FileInfo().IsDir() { + rc, err := zf.Open() + if err != nil { + return fmt.Errorf("could not open file in zip: %w", err) + } + binPath = filepath.Join(tmpDir, binaryName) + out, err := os.Create(binPath) + if err != nil { + rc.Close() + return fmt.Errorf("could not create binary file: %w", err) + } + if _, err := io.Copy(out, rc); err != nil { + out.Close() + rc.Close() + return fmt.Errorf("could not extract binary: %w", err) + } + out.Close() + rc.Close() + if err := os.Chmod(binPath, 0755); err != nil { + return fmt.Errorf("could not make binary executable: %w", err) + } + break + } + } } else { // For non-archive assets, assume the asset is the binary itself. binPath = assetPath @@ -2033,7 +2071,16 @@ func runUpdateCLI() error { in.Close() out.Close() - // Attempt to atomically replace + // On Windows, a running executable cannot be overwritten directly. + // Move the old binary out of the way first, then rename the new one in. + if runtime.GOOS == "windows" { + oldPath := execPath + ".old" + _ = os.Remove(oldPath) // clean up any previous leftover + if err := os.Rename(execPath, oldPath); err != nil { + return fmt.Errorf("could not move old executable out of the way: %w", err) + } + } + if err := os.Rename(tmpNew, execPath); err != nil { return fmt.Errorf("could not replace executable: %w", err) } diff --git a/view/html.go b/view/html.go index 74f87b782e0e1746676538469461827a2d018b83..52f5102968cc3fab696ae80fea36730cc3fed0d9 100644 --- a/view/html.go +++ b/view/html.go @@ -23,7 +23,6 @@ import ( "github.com/floatpane/matcha/theme" "github.com/yuin/goldmark" "github.com/yuin/goldmark/renderer/html" - "golang.org/x/sys/unix" ) func linkStyle() lipgloss.Style { @@ -57,32 +56,6 @@ func getTerminalCellSize() int { return defaultCellHeight } -// getCellHeightFromFd attempts to get the terminal cell height from a file descriptor. -// Returns 0 if it fails or if pixel dimensions are not available. -func getCellHeightFromFd(fd int) int { - ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) - if err != nil { - return 0 - } - - // ws.Row = number of character rows - // ws.Ypixel = height in pixels - // Some terminals don't report pixel dimensions (return 0) - if ws.Row > 0 && ws.Ypixel > 0 { - cellHeight := int(ws.Ypixel) / int(ws.Row) - if cellHeight > 0 { - debugImageProtocol("terminal cell height: %d pixels (rows=%d, ypixel=%d, fd=%d)", cellHeight, ws.Row, ws.Ypixel, fd) - return cellHeight - } - } - - // Terminal reported dimensions but no pixel info - this is common - if ws.Row > 0 && ws.Ypixel == 0 { - debugImageProtocol("terminal fd=%d has rows=%d but no pixel info (ypixel=0)", fd, ws.Row) - } - - return 0 -} // hyperlinkSupported checks if the terminal supports OSC 8 hyperlinks. func hyperlinkSupported() bool { @@ -129,7 +102,8 @@ func hyperlinkSupported() bool { // Check for specific environment variables that indicate hyperlink support if os.Getenv("KITTY_WINDOW_ID") != "" || os.Getenv("GHOSTTY_RESOURCES_DIR") != "" || - os.Getenv("WEZTERM_EXECUTABLE") != "" { + os.Getenv("WEZTERM_EXECUTABLE") != "" || + os.Getenv("WT_SESSION") != "" { return true } diff --git a/view/terminal_unix.go b/view/terminal_unix.go new file mode 100644 index 0000000000000000000000000000000000000000..09aa3a3dd9eb10776f7097f09810127dd079c506 --- /dev/null +++ b/view/terminal_unix.go @@ -0,0 +1,32 @@ +//go:build !windows + +package view + +import "golang.org/x/sys/unix" + +// getCellHeightFromFd attempts to get the terminal cell height from a file descriptor. +// Returns 0 if it fails or if pixel dimensions are not available. +func getCellHeightFromFd(fd int) int { + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { + return 0 + } + + // ws.Row = number of character rows + // ws.Ypixel = height in pixels + // Some terminals don't report pixel dimensions (return 0) + if ws.Row > 0 && ws.Ypixel > 0 { + cellHeight := int(ws.Ypixel) / int(ws.Row) + if cellHeight > 0 { + debugImageProtocol("terminal cell height: %d pixels (rows=%d, ypixel=%d, fd=%d)", cellHeight, ws.Row, ws.Ypixel, fd) + return cellHeight + } + } + + // Terminal reported dimensions but no pixel info - this is common + if ws.Row > 0 && ws.Ypixel == 0 { + debugImageProtocol("terminal fd=%d has rows=%d but no pixel info (ypixel=0)", fd, ws.Row) + } + + return 0 +} diff --git a/view/terminal_windows.go b/view/terminal_windows.go new file mode 100644 index 0000000000000000000000000000000000000000..5d751da55d975530c4329075aede547b482d00c3 --- /dev/null +++ b/view/terminal_windows.go @@ -0,0 +1,9 @@ +//go:build windows + +package view + +// getCellHeightFromFd is not supported on Windows. +// Returns 0 to fall back to the default cell height. +func getCellHeightFromFd(fd int) int { + return 0 +}