feature: Add basic EDID reading, writing, and patching capabilities
This commit is contained in:
parent
d0a4d26082
commit
183d2606dc
12 changed files with 431 additions and 5 deletions
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package config
|
||||
|
||||
import _ "embed"
|
||||
|
72
app/edidtools/edid_parser.go
Normal file
72
app/edidtools/edid_parser.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package edidtools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
edidparser "github.com/anoopengineer/edidparser/edid"
|
||||
)
|
||||
|
||||
func ParseEDID(rawEDIDFile []byte, allowUnsupportedDevices bool) (*DisplayMetadata, error) {
|
||||
parsedEDID, err := edidparser.NewEdid(rawEDIDFile)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse EDID file: %w", err)
|
||||
}
|
||||
|
||||
for manufacturer, manufacturerSupportedDevices := range QuirksRegistry {
|
||||
if parsedEDID.ManufacturerId == manufacturer {
|
||||
if deviceQuirks, ok := manufacturerSupportedDevices[parsedEDID.MonitorName]; ok || allowUnsupportedDevices {
|
||||
maxWidth := 0
|
||||
maxHeight := 0
|
||||
maxRefreshRate := 0
|
||||
|
||||
for _, resolution := range parsedEDID.DetailedTimingDescriptors {
|
||||
if int(resolution.HorizontalActive) > maxWidth && int(resolution.VerticalActive) > maxHeight {
|
||||
maxWidth = int(resolution.HorizontalActive)
|
||||
maxHeight = int(resolution.VerticalActive)
|
||||
}
|
||||
|
||||
// Convert pixel clock to refresh rate
|
||||
// Refresh Rate = Pixel Clock / ((Horizontal Active + Horizontal Blanking) * (Vertical Active + Vertical Blanking))
|
||||
hTotal := int(resolution.HorizontalActive + resolution.HorizontalBlanking)
|
||||
vTotal := int(resolution.VerticalActive + resolution.VerticalBlanking)
|
||||
refreshRate := int(int(resolution.PixelClock*1000) / (hTotal * vTotal))
|
||||
|
||||
if refreshRate > maxRefreshRate {
|
||||
maxRefreshRate = refreshRate
|
||||
}
|
||||
}
|
||||
|
||||
if maxWidth == 0 || maxHeight == 0 {
|
||||
if deviceQuirks.MaxWidth == 0 || deviceQuirks.MaxHeight == 0 {
|
||||
return nil, fmt.Errorf("failed to determine maximum resolution for monitor '%s'", parsedEDID.MonitorName)
|
||||
}
|
||||
|
||||
maxWidth = deviceQuirks.MaxWidth
|
||||
maxHeight = deviceQuirks.MaxHeight
|
||||
}
|
||||
|
||||
if maxRefreshRate == 0 {
|
||||
if deviceQuirks.MaxRefreshRate == 0 {
|
||||
return nil, fmt.Errorf("failed to determine maximum refresh rate for monitor '%s'", parsedEDID.MonitorName)
|
||||
}
|
||||
|
||||
maxRefreshRate = deviceQuirks.MaxRefreshRate
|
||||
}
|
||||
|
||||
displayMetadata := &DisplayMetadata{
|
||||
EDID: rawEDIDFile,
|
||||
DeviceVendor: parsedEDID.ManufacturerId,
|
||||
DeviceQuirks: deviceQuirks,
|
||||
MaxWidth: maxWidth,
|
||||
MaxHeight: maxHeight,
|
||||
MaxRefreshRate: maxRefreshRate,
|
||||
}
|
||||
|
||||
return displayMetadata, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to match manufacturer for monitor vendor: '%s'", parsedEDID.ManufacturerId)
|
||||
}
|
127
app/edidtools/patching_tools_linux.go
Normal file
127
app/edidtools/patching_tools_linux.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package edidtools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
// Attempts to fetch the EDID firmware for any supported XR glasses device
|
||||
func FetchXRGlassEDID(allowUnsupportedDevices bool) (*DisplayMetadata, error) {
|
||||
// Implementation goes here
|
||||
pciDeviceCommand, err := exec.Command("lspci").Output()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute lspci command: %w", err)
|
||||
}
|
||||
|
||||
pciDevices := strings.Split(string(pciDeviceCommand), "\n")
|
||||
pciDevices = pciDevices[:len(pciDevices)-1]
|
||||
|
||||
vgaDevices := []string{}
|
||||
|
||||
for _, pciDevice := range pciDevices {
|
||||
if strings.Contains(pciDevice, "VGA compatible controller:") {
|
||||
vgaDevices = append(vgaDevices, pciDevice[:strings.Index(pciDevice, " ")])
|
||||
}
|
||||
}
|
||||
|
||||
for _, vgaDevice := range vgaDevices {
|
||||
cardDevices, err := os.ReadDir("/sys/devices/pci0000:00/0000:" + vgaDevice + "/drm/")
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read directory for device '%s': %w", vgaDevice, err)
|
||||
}
|
||||
|
||||
for _, cardDevice := range cardDevices {
|
||||
if !strings.Contains(cardDevice.Name(), "card") {
|
||||
continue
|
||||
}
|
||||
|
||||
monitors, err := os.ReadDir("/sys/devices/pci0000:00/0000:" + vgaDevice + "/drm/" + cardDevice.Name())
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read directory for card device '%s': %w", cardDevice.Name(), err)
|
||||
}
|
||||
|
||||
for _, monitor := range monitors {
|
||||
if !strings.Contains(monitor.Name(), cardDevice.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
rawEDIDFile, err := os.ReadFile("/sys/devices/pci0000:00/0000:" + vgaDevice + "/drm/" + cardDevice.Name() + "/" + monitor.Name() + "/edid")
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read EDID file for monitor '%s': %w", monitor.Name(), err)
|
||||
}
|
||||
|
||||
if len(rawEDIDFile) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
parsedEDID, err := ParseEDID(rawEDIDFile, allowUnsupportedDevices)
|
||||
|
||||
if err != nil {
|
||||
if !strings.HasPrefix(err.Error(), "failed to match manufacturer for monitor vendor") {
|
||||
log.Warnf("Failed to parse EDID for monitor '%s': %s", monitor.Name(), err.Error())
|
||||
}
|
||||
} else {
|
||||
parsedEDID.LinuxDRMCard = cardDevice.Name()
|
||||
parsedEDID.LinuxDRMConnector = strings.Replace(cardDevice.Name(), cardDevice.Name()+"-", "", 1)
|
||||
|
||||
return parsedEDID, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("could not find supported device! Check if the XR device is plugged in. If it is plugged in and working correctly, check the README or open an issue.")
|
||||
}
|
||||
|
||||
// Loads custom firmware for a supported XR glass device
|
||||
func LoadCustomEDIDFirmware(displayMetadata *DisplayMetadata, edidFirmware []byte) error {
|
||||
if displayMetadata.LinuxDRMCard == "" || displayMetadata.LinuxDRMConnector == "" {
|
||||
return fmt.Errorf("missing Linux DRM card or connector information")
|
||||
}
|
||||
|
||||
drmFile, err := os.Open("/sys/kernel/debug/dri/" + strings.Replace(displayMetadata.LinuxDRMCard, "card", "", 1) + "/" + displayMetadata.LinuxDRMConnector + "/edid_override")
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open EDID override file for monitor '%s': %w", displayMetadata.LinuxDRMConnector, err)
|
||||
}
|
||||
|
||||
defer drmFile.Close()
|
||||
|
||||
if _, err := drmFile.Write(edidFirmware); err != nil {
|
||||
return fmt.Errorf("failed to write EDID firmware for monitor '%s': %w", displayMetadata.LinuxDRMConnector, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unloads custom firmware for a supported XR glass device
|
||||
func UnloadCustomEDIDFirmware(displayMetadata *DisplayMetadata) error {
|
||||
if displayMetadata.LinuxDRMCard == "" || displayMetadata.LinuxDRMConnector == "" {
|
||||
return fmt.Errorf("missing Linux DRM card or connector information")
|
||||
}
|
||||
|
||||
drmFile, err := os.Open("/sys/kernel/debug/dri/" + strings.Replace(displayMetadata.LinuxDRMCard, "card", "", 1) + "/" + displayMetadata.LinuxDRMConnector + "/edid_override")
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open EDID override file for monitor '%s': %w", displayMetadata.LinuxDRMConnector, err)
|
||||
}
|
||||
|
||||
defer drmFile.Close()
|
||||
|
||||
if _, err := drmFile.Write([]byte("reset")); err != nil {
|
||||
return fmt.Errorf("failed to unload EDID firmware for monitor '%s': %w", displayMetadata.LinuxDRMConnector, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
21
app/edidtools/patching_tools_macos.go
Normal file
21
app/edidtools/patching_tools_macos.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package edidtools
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Attempts to fetch the EDID firmware for any supported XR glasses device
|
||||
func FetchXRGlassEDID(allowUnsupportedDevices bool) (*DisplayMetadata, error) {
|
||||
return nil, fmt.Errorf("automatic fetching of EDID data is not supported on macOS")
|
||||
}
|
||||
|
||||
// Loads custom firmware for a supported XR glass device
|
||||
func LoadCustomEDIDFirmware(displayMetadata *DisplayMetadata, edidFirmware []byte) error {
|
||||
return fmt.Errorf("loading custom EDID firmware is not supported on macOS")
|
||||
}
|
||||
|
||||
// Unloads custom firmware for a supported XR glass device
|
||||
func UnloadCustomEDIDFirmware(displayMetadata *DisplayMetadata) error {
|
||||
return fmt.Errorf("unloading custom EDID firmware is not supported on macOS")
|
||||
}
|
21
app/edidtools/patching_tools_win.go
Normal file
21
app/edidtools/patching_tools_win.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package edidtools
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Attempts to fetch the EDID firmware for any supported XR glasses device
|
||||
func FetchXRGlassEDID(allowUnsupportedDevices bool) (*DisplayMetadata, error) {
|
||||
return nil, fmt.Errorf("automatic fetching of EDID data is not supported on Windows")
|
||||
}
|
||||
|
||||
// Loads custom firmware for a supported XR glass device
|
||||
func LoadCustomEDIDFirmware(displayMetadata *DisplayMetadata, edidFirmware []byte) error {
|
||||
return fmt.Errorf("loading custom EDID firmware is not supported on Windows")
|
||||
}
|
||||
|
||||
// Unloads custom firmware for a supported XR glass device
|
||||
func UnloadCustomEDIDFirmware(displayMetadata *DisplayMetadata) error {
|
||||
return fmt.Errorf("unloading custom EDID firmware is not supported on Windows")
|
||||
}
|
14
app/edidtools/quirks.go
Normal file
14
app/edidtools/quirks.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package edidtools
|
||||
|
||||
// Vendor and devices names sourced from "https://uefi.org/uefi-pnp-export"
|
||||
var QuirksRegistry = map[string]map[string]DisplayQuirks{
|
||||
"MRG": {
|
||||
"Air": {
|
||||
MaxWidth: 1920,
|
||||
MaxHeight: 1080,
|
||||
MaxRefreshRate: 120,
|
||||
SensorInitDelay: 10,
|
||||
ZVectorDisabled: true,
|
||||
},
|
||||
},
|
||||
}
|
20
app/edidtools/struct.go
Normal file
20
app/edidtools/struct.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package edidtools
|
||||
|
||||
type DisplayQuirks struct {
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
MaxRefreshRate int
|
||||
SensorInitDelay int
|
||||
ZVectorDisabled bool
|
||||
}
|
||||
|
||||
type DisplayMetadata struct {
|
||||
EDID []byte
|
||||
DeviceVendor string
|
||||
DeviceQuirks DisplayQuirks
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
MaxRefreshRate int
|
||||
LinuxDRMCard string
|
||||
LinuxDRMConnector string
|
||||
}
|
51
app/main.go
51
app/main.go
|
@ -1,11 +1,15 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
libconfig "git.terah.dev/UnrealXR/unrealxr/app/config"
|
||||
"git.terah.dev/UnrealXR/unrealxr/app/edidtools"
|
||||
"git.terah.dev/UnrealXR/unrealxr/edidpatcher"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/kirsle/configdir"
|
||||
|
@ -35,7 +39,7 @@ func mainEntrypoint(context.Context, *cli.Command) error {
|
|||
|
||||
if err != nil {
|
||||
log.Debug("Creating default config file")
|
||||
err := os.WriteFile(path.Join(configDir, "config.yml"), InitialConfig, 0644)
|
||||
err := os.WriteFile(path.Join(configDir, "config.yml"), libconfig.InitialConfig, 0644)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create initial config file: %w", err)
|
||||
|
@ -49,14 +53,53 @@ func mainEntrypoint(context.Context, *cli.Command) error {
|
|||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
config := &Config{}
|
||||
config := &libconfig.Config{}
|
||||
err = yaml.Unmarshal(configBytes, config)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
InitializePotentiallyMissingConfigValues(config)
|
||||
libconfig.InitializePotentiallyMissingConfigValues(config)
|
||||
log.Info("Attempting to read display EDID file and fetch metadata")
|
||||
|
||||
displayMetadata, err := edidtools.FetchXRGlassEDID(*config.Overrides.AllowUnsupportedDevices)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch EDID or get metadata: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Got EDID file and metadata")
|
||||
log.Info("Patching EDID firmware to be specialized")
|
||||
|
||||
patchedFirmware, err := edidpatcher.PatchEDIDToBeSpecialized(displayMetadata.EDID)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to patch EDID firmware: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Uploading patched EDID firmware")
|
||||
err = edidtools.LoadCustomEDIDFirmware(displayMetadata, patchedFirmware)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to upload patched EDID firmware: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := edidtools.UnloadCustomEDIDFirmware(displayMetadata)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to unload custom EDID firmware: %s", err.Error())
|
||||
}
|
||||
|
||||
log.Info("Please unplug and plug in your XR device to restore it back to normal settings.")
|
||||
}()
|
||||
|
||||
fmt.Print("Press the Enter key to continue loading after you unplug and plug in your XR device.")
|
||||
bufio.NewReader(os.Stdin).ReadBytes('\n') // Wait for Enter key press before continuing
|
||||
|
||||
log.Info("Initializing XR headset")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -90,6 +133,6 @@ func main() {
|
|||
}
|
||||
|
||||
if err := cmd.Run(context.Background(), os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("Fatal error during execution: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
|
102
edidpatcher/edidpatcher.go
Normal file
102
edidpatcher/edidpatcher.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package edidpatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Calculates a checksum for a given EDID block (base EDID, extension blocks, etc.)
|
||||
func CalculateEDIDChecksum(edidBlock []byte) byte {
|
||||
sum := 0
|
||||
|
||||
for _, value := range edidBlock[:len(edidBlock)-1] {
|
||||
sum += int(value)
|
||||
}
|
||||
|
||||
return byte((-sum) & 0xFF)
|
||||
}
|
||||
|
||||
var MSFTPayloadSize = byte(22 + 4)
|
||||
|
||||
// Patch a given EDID to be a "specialized display", allowing for the display to be used by third-party window-managers/compositors/applications directly.
|
||||
func PatchEDIDToBeSpecialized(edid []byte) ([]byte, error) {
|
||||
newEDID := make([]byte, len(edid))
|
||||
copy(newEDID, edid)
|
||||
|
||||
isAnEnhancedEDID := len(newEDID) > 128
|
||||
|
||||
foundExtensionBase := 0
|
||||
extensionBaseExists := false
|
||||
|
||||
// Find an appropriate extension base
|
||||
if isAnEnhancedEDID {
|
||||
for currentExtensionPosition := 128; currentExtensionPosition < len(newEDID); currentExtensionPosition += 128 {
|
||||
if newEDID[currentExtensionPosition] != 0x02 {
|
||||
continue
|
||||
}
|
||||
|
||||
if newEDID[currentExtensionPosition+1] != 0x03 {
|
||||
log.Warn("Incompatible version detected for ANSI CTA data section in EDID")
|
||||
}
|
||||
|
||||
foundExtensionBase = currentExtensionPosition
|
||||
extensionBaseExists = true
|
||||
}
|
||||
|
||||
if foundExtensionBase == 0 {
|
||||
foundExtensionBase = len(newEDID)
|
||||
newEDID = append(newEDID, make([]byte, 128)...)
|
||||
}
|
||||
} else {
|
||||
foundExtensionBase = 128
|
||||
newEDID = append(newEDID, make([]byte, 128)...)
|
||||
}
|
||||
|
||||
newEDID[foundExtensionBase+2] = MSFTPayloadSize
|
||||
|
||||
if !extensionBaseExists {
|
||||
// Add another extension to the original EDID
|
||||
if newEDID[126] == 255 {
|
||||
return nil, fmt.Errorf("EDID extension block limit reached, but we need to add another extension")
|
||||
}
|
||||
|
||||
newEDID[126] += 1
|
||||
newEDID[127] = CalculateEDIDChecksum(newEDID[:128])
|
||||
|
||||
newEDID[foundExtensionBase] = 0x02
|
||||
newEDID[foundExtensionBase+1] = 0x03
|
||||
newEDID[foundExtensionBase+3] = 0x00
|
||||
} else {
|
||||
if newEDID[foundExtensionBase+2] != MSFTPayloadSize && newEDID[foundExtensionBase+2] != 0 {
|
||||
currentBase := newEDID[foundExtensionBase+2]
|
||||
|
||||
copy(newEDID[foundExtensionBase+4:foundExtensionBase+int(currentBase)-1], make([]byte, int(currentBase)-1))
|
||||
copy(newEDID[foundExtensionBase+int(MSFTPayloadSize):foundExtensionBase+127], newEDID[foundExtensionBase+int(currentBase):foundExtensionBase+127])
|
||||
}
|
||||
}
|
||||
|
||||
generatedUUID := uuid.New()
|
||||
uuidBytes, err := generatedUUID.MarshalBinary()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal UUID: %w", err)
|
||||
}
|
||||
|
||||
// Implemented using https://learn.microsoft.com/en-us/windows-hardware/drivers/display/specialized-monitors-edid-extension
|
||||
// VST & Length
|
||||
newEDID[foundExtensionBase+4] = 0x3<<5 | 0x15 // 0x3: vendor specific tag; 0x15: length
|
||||
// Assigned IEEE OUI
|
||||
newEDID[foundExtensionBase+5] = 0x5C
|
||||
newEDID[foundExtensionBase+6] = 0x12
|
||||
newEDID[foundExtensionBase+7] = 0xCA
|
||||
// Actual data
|
||||
newEDID[foundExtensionBase+8] = 0x2 // Using version 0x2 for better compatibility
|
||||
newEDID[foundExtensionBase+9] = 0x7 // Using VR tag for better compatibility even though it probably doesn't matter
|
||||
copy(newEDID[foundExtensionBase+10:foundExtensionBase+10+16], uuidBytes)
|
||||
|
||||
newEDID[foundExtensionBase+127] = CalculateEDIDChecksum(newEDID[foundExtensionBase : foundExtensionBase+127])
|
||||
|
||||
return newEDID, nil
|
||||
}
|
2
go.mod
2
go.mod
|
@ -3,6 +3,7 @@ module git.terah.dev/UnrealXR/unrealxr
|
|||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
|
@ -12,6 +13,7 @@ require (
|
|||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
|
|
4
go.sum
4
go.sum
|
@ -1,3 +1,5 @@
|
|||
github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0 h1:rTfysyBCL7LPbq9GFpQbllvKT8vEI93lQUwksMMxHMI=
|
||||
github.com/anoopengineer/edidparser v0.0.0-20240602223913-86ca9ed3d2b0/go.mod h1:fEt61NePh3ZMxA+g3iC4CaGzY9lEsHRUkYJY2x0lBAw=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
|
@ -16,6 +18,8 @@ github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi
|
|||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f h1:dKccXx7xA56UNqOcFIbuqFjAWPVtP688j5QMgmo6OHU=
|
||||
github.com/kirsle/configdir v0.0.0-20170128060238-e45d2f54772f/go.mod h1:4rEELDSfUAlBSyUjPG0JnaNGjf13JySHFeRdD/3dLP0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue