Skip to content

Implemented click to open report folder/report file - #2

Closed
iUnstable0 wants to merge 1 commit into
DatCodeMania:mainfrom
iUnstable0:main
Closed

Implemented click to open report folder/report file#2
iUnstable0 wants to merge 1 commit into
DatCodeMania:mainfrom
iUnstable0:main

Conversation

@iUnstable0

Copy link
Copy Markdown
Contributor

No description provided.

Copilot AI review requested due to automatic review settings July 31, 2026 16:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds interactive “open report / open folder” affordances to the TUI’s final (completed) running screen by rendering OSC 8 hyperlinks and capturing plain left-clicks to launch the report file or reveal it in the OS file manager.

Changes:

  • Render the final-frame report path and “↗ open report / ↗ open folder” as OSC 8 hyperlinks, plus record hit-boxes for plain-click activation.
  • Add cross-platform native openers (explorer.exe on Windows; open/xdg-open elsewhere) and hook click handling into the running screen update loop.
  • Add tests and documentation describing hyperlink + click behavior.

Reviewed changes

Copilot reviewed 7 out of 15 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tui.go Adds OSC 8 hyperlink helpers, file:// URL conversion, and button hit-box rendering helpers.
app.go Enables mouse capture on the final frame, handles report-button clicks, and renders clickable report path + buttons.
tui_test.go Adds tests for hyperlinks, hit-box accuracy, click behavior, overflow row shifting, and open-error surfacing.
open_windows.go Implements Windows native open/reveal via Explorer.
open_other.go Implements macOS/Linux/BSD native open/reveal via open / xdg-open.
main.go Prints a clickable (or plain, when redirected) report path for non-TUI output.
README.md Documents clickable report path/buttons and selection behavior on the final frame.
.idea/vcs.xml Adds JetBrains IDE metadata (should not be committed).
.idea/modules.xml Adds JetBrains IDE metadata (should not be committed).
.idea/golinter.xml Adds JetBrains IDE metadata (should not be committed).
.idea/go.imports.xml Adds JetBrains IDE metadata (should not be committed).
.idea/discord.xml Adds JetBrains IDE metadata (should not be committed).
.idea/discord-delete.iml Adds JetBrains IDE metadata (should not be committed).
.idea/.gitignore Adds JetBrains IDE metadata (should not be committed).
.gitignore Minor update; should also ignore .idea/ to prevent IDE files from being re-added.
Files not reviewed (7)
  • .idea/.gitignore: Generated file
  • .idea/discord-delete.iml: Generated file
  • .idea/discord.xml: Generated file
  • .idea/go.imports.xml: Generated file
  • .idea/golinter.xml: Generated file
  • .idea/modules.xml: Generated file
  • .idea/vcs.xml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app.go Outdated
Comment on lines +925 to +928
m.openErr = ""
if err := openPath(h.path, h.reveal); err != nil {
m.openErr = "could not open " + h.path + ": " + err.Error()
}
Comment thread .idea/vcs.xml Outdated
Comment on lines +1 to +6
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project> No newline at end of file
Comment thread .idea/modules.xml Outdated
Comment on lines +1 to +8
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/discord-delete.iml" filepath="$PROJECT_DIR$/.idea/discord-delete.iml" />
</modules>
</component>
</project> No newline at end of file
Comment thread .idea/golinter.xml Outdated
Comment on lines +1 to +7
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoLinterSettings">
<option name="customConfigFile" value="$PROJECT_DIR$/.golangci.yml" />
<option name="useCustomConfigFile" value="true" />
</component>
</project> No newline at end of file
Comment thread .idea/go.imports.xml Outdated
Comment on lines +1 to +10
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project> No newline at end of file
Comment thread .idea/discord.xml Outdated
Comment on lines +1 to +14
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
<option name="applicationTheme" value="default" />
<option name="iconsTheme" value="default" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
<option name="customApplicationId" value="" />
</component>
</project> No newline at end of file
Comment thread .idea/.gitignore Outdated
Comment on lines +1 to +10
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
Comment thread .idea/discord-delete.iml Outdated
Comment on lines +1 to +9
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="GoModuleSettings" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module> No newline at end of file
@DatCodeMania

DatCodeMania commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Hi, thanks for the PR.

A few things before I can merge this.

The .idea/ files need to come out of the commit - you may add a line into .gitignore. Please also restore the trailing newline in .gitignore.

On the design: I'd rather this open the report itself in whatever app handles .txt than in a file manager. It is cleaner both in terms of code and usability. My implementation is at the bottom of this comment.

The mouse integration is well done, but a keybind ('o'?) may be a reasonable fallback.

Finally, a note on comments. They are more verbose than the rest of the repository, which leans terse and technical: a comment should say something the code does not, and explain why rather than what. You beat me to making a PR before I could write a CONTRIBUTING.md to clarify this. Please match the surrounding files.

I'll take a more thorough look at the rest if you can implement these.

Thanks again.


My implementation.

open.go

package main

import (
	"errors"

	tea "github.com/charmbracelet/bubbletea"
)

// errNoEditor is returned when neither VISUAL nor EDITOR names a program.
var errNoEditor = errors.New("no VISUAL or EDITOR set")

// openDoneMsg carries the outcome of an openReport command.
type openDoneMsg struct{ err error }

// openReport opens the report in the desktop's handler where there is a desktop,
// otherwise the terminal editor.
func openReport(path string) tea.Cmd {
	if hasDesktop() {
		return func() tea.Msg { return openDoneMsg{openDetached(path)} }
	}
	ed := terminalEditor(path)
	if ed == nil {
		return func() tea.Msg { return openDoneMsg{errNoEditor} }
	}
	return tea.ExecProcess(ed, func(err error) tea.Msg { return openDoneMsg{err} })
}

open_other.go

//go:build !windows

package main

import (
	"cmp"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"syscall"
)

// hasDesktop reports whether a graphical session is reachable. xdg-open gates its
// MIME dispatch on these two variables and falls through to a web browser without
// them. macOS sets neither even with a desktop present.
func hasDesktop() bool {
	if runtime.GOOS == "darwin" {
		return true
	}
	return os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != ""
}

// terminalEditor builds the command for $VISUAL or $EDITOR, nil if neither is set.
func terminalEditor(path string) *exec.Cmd {
	spec := cmp.Or(os.Getenv("VISUAL"), os.Getenv("EDITOR"))
	if strings.TrimSpace(spec) == "" {
		return nil
	}
	return exec.Command("/bin/sh", "-c", spec+` "$@"`, "sh", path)
}

// openDetached hands path to the desktop's handler for its type.
func openDetached(path string) error {
	name := "xdg-open"
	if runtime.GOOS == "darwin" {
		name = "open"
	}
	cmd := exec.Command(name, path)
	// xdg-open runs a Terminal=true handler on the caller's tty by design, so the
	// child gets its own session to keep it away from the TUI.
	cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
	if err := cmd.Start(); err != nil {
		return err
	}
	// xdg-open does not fork the handler off, so the child outlives this call and
	// is reaped off the update path.
	go func() { _ = cmd.Wait() }()
	return nil
}

open_windows.go

//go:build windows

package main

import (
	"os/exec"

	"golang.org/x/sys/windows"
)

// hasDesktop is unconditionally true on Windows, where ShellExecute resolves a
// handler without consulting a display variable.
func hasDesktop() bool { return true }

// terminalEditor is unreachable on Windows, where hasDesktop keeps openReport on
// the desktop branch.
func terminalEditor(string) *exec.Cmd { return nil }

// openDetached asks the shell to open path with its registered handler. Going
// through ShellExecute rather than cmd.exe leaves & and ^ in the path needing no
// escaping, and spawns no child to reap.
func openDetached(path string) error {
	p, err := windows.UTF16PtrFromString(path)
	if err != nil {
		return err
	}
	return windows.ShellExecute(0, nil, p, nil, nil, windows.SW_SHOWNORMAL)
}

The end-of-run frame prints the report path as an OSC 8 link and draws an
open button next to it. Terminals only follow hyperlinks on ctrl/cmd+click,
so the frame records where the button sits and the mouse is captured for
that frame alone, leaving text selection alone during the run. Pressing o
does the same for terminals that report no mouse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@DatCodeMania

Copy link
Copy Markdown
Owner

Hi, thanks again for your PR.

I've just pushed it to main with you credited as co-author on the commit. I swapped the opener and adjusted a few things around your code and the documentation.

Closing this since it's in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants