Files
2026-08-03 23:29:16 -05:00

184 lines
4.6 KiB
Go

//go:build windows
package capture
import (
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"unsafe"
)
type ShellDropHit struct {
Directory string
ItemName string
Desktop bool
}
func ResolveShellDropHit(x, y int32) ShellDropHit {
result := make(chan ShellDropHit, 1)
go func() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
out := ShellDropHit{}
hr, _, _ := procCoInitializeEx.Call(0, COINIT_MULTITHREADED)
if hr != 0 && hr != 1 {
result <- out
return
}
defer procCoUninitialize.Call()
var automation *iuiAutomation
hr, _, _ = procCoCreateInstance.Call(
uintptr(unsafe.Pointer(&CLSID_CUIAutomation)), 0, CLSCTX_INPROC_SERVER,
uintptr(unsafe.Pointer(&IID_IUIAutomation)), uintptr(unsafe.Pointer(&automation)),
)
if hr != S_OK || automation == nil {
result <- out
return
}
defer automation.Release()
walker := automation.GetControlViewWalker()
if walker == nil {
result <- out
return
}
defer walker.Release()
elem := automation.ElementFromPoint(point{x: x, y: y})
if elem == nil {
result <- out
return
}
out.ItemName = strings.TrimSpace(elem.GetCurrentName())
sawCabinet := false
sawShellList := false
for current, depth := elem, 0; current != nil && depth < 32; depth++ {
className := current.GetCurrentClassName()
if className == "CabinetWClass" {
sawCabinet = true
out.Directory = explorerDirectoryFromUIA(walker, current)
}
if className == "SysListView32" || className == "SHELLDLL_DefView" {
sawShellList = true
}
parent := walker.GetParentElement(current)
current.Release()
current = parent
}
if sawCabinet {
out.Desktop = false
} else if sawShellList {
out.Desktop = true
if home, err := os.UserHomeDir(); err == nil {
out.Directory = filepath.Join(home, "Desktop")
}
}
result <- out
}()
select {
case hit := <-result:
return hit
case <-time.After(4 * time.Second):
return ShellDropHit{}
}
}
func explorerDirectoryFromUIA(walker *iuiAutomationTreeWalker, root *iuiAutomationElement) string {
visited := 0
var walk func(*iuiAutomationElement) string
walk = func(parent *iuiAutomationElement) string {
for child := shellDropFirstChild(walker, parent); child != nil && visited < 2500; {
visited++
next := shellDropNextSibling(walker, child)
name := strings.TrimSpace(child.GetCurrentName())
if strings.HasPrefix(strings.ToLower(name), "address:") {
candidate := shellDirectoryCandidate(name)
if candidate == "" {
// Continue into the element; localized or virtual shell paths
// may not be filesystem destinations.
} else {
child.Release()
if next != nil {
next.Release()
}
return candidate
}
}
if pattern := child.GetCurrentPattern(uiaValuePatternID); pattern != nil {
valuePattern := (*iuiAutomationValuePattern)(unsafe.Pointer(pattern))
candidate := shellDirectoryCandidate(valuePattern.GetCurrentValue())
valuePattern.Release()
if candidate != "" {
child.Release()
if next != nil {
next.Release()
}
return candidate
}
}
if candidate := walk(child); candidate != "" {
child.Release()
if next != nil {
next.Release()
}
return candidate
}
child.Release()
child = next
}
return ""
}
return walk(root)
}
func shellDropFirstChild(walker *iuiAutomationTreeWalker, elem *iuiAutomationElement) *iuiAutomationElement {
var child *iuiAutomationElement
hr := callSyscallN(walker.lpVtbl.GetFirstChildElement,
uintptr(unsafe.Pointer(walker)),
uintptr(unsafe.Pointer(elem)),
uintptr(unsafe.Pointer(&child)),
)
if hr != S_OK || child == nil {
return nil
}
return child
}
func shellDropNextSibling(walker *iuiAutomationTreeWalker, elem *iuiAutomationElement) *iuiAutomationElement {
var sibling *iuiAutomationElement
hr := callSyscallN(walker.lpVtbl.GetNextSiblingElement,
uintptr(unsafe.Pointer(walker)),
uintptr(unsafe.Pointer(elem)),
uintptr(unsafe.Pointer(&sibling)),
)
if hr != S_OK || sibling == nil {
return nil
}
return sibling
}
func shellDirectoryCandidate(value string) string {
value = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(value), "Address:"))
if strings.HasPrefix(strings.ToLower(value), "file:") {
if parsed, err := url.Parse(value); err == nil {
value = parsed.Path
if len(value) >= 3 && value[0] == '/' && value[2] == ':' {
value = value[1:]
}
value, _ = url.PathUnescape(value)
}
}
if !filepath.IsAbs(value) {
return ""
}
if info, err := os.Stat(value); err == nil && info.IsDir() {
return filepath.Clean(value)
}
return ""
}