diff --git a/v3/DEVELOPMENT.md b/v3/DEVELOPMENT.md index 1c7a9e365..22c41fd39 100644 --- a/v3/DEVELOPMENT.md +++ b/v3/DEVELOPMENT.md @@ -78,4 +78,36 @@ When adding a common event, ensure that the platform specific events are mapped. }) ``` -NOTE: We may try to automate this in the future by adding the mapping to the event definition. \ No newline at end of file +NOTE: We may try to automate this in the future by adding the mapping to the event definition. + +### Plugins + +Plugins are a way to extend the functionality of your Wails application. + +#### Creating a plugin + +Plugins are standard Go structure that adhere to the following interface: + +```go +type Plugin interface { + Name() string + Init(*application.App) error + Shutdown() + CallableByJS() []string + InjectJS() string +} +``` + +The `Name()` method returns the name of the plugin. This is used for logging purposes. + +The `Init(*application.App) error` method is called when the plugin is loaded. The `*application.App` +parameter is the application that the plugin is being loaded into. Any errors will prevent +the application from starting. + +The `Shutdown()` method is called when the application is shutting down. + +The `CallableByJS()` method returns a list of exported functions that can be called from +the frontend. These method names must exactly match the names of the methods exported +by the plugin. + +The `InjectJS()` method returns JavaScript that should be injected into all windows as they are created. This is useful for adding custom JavaScript functions that complement the plugin. diff --git a/v3/STATUS.md b/v3/STATUS.md index 784f4a54b..02dc12871 100644 --- a/v3/STATUS.md +++ b/v3/STATUS.md @@ -90,7 +90,7 @@ Webview Window Interface Methods | Feature | Windows | Linux | Mac | Notes | |---------|---------|-------|-----|-------| -| Quit | Y | Y | Y | | +| Quit | Y | Y | Y | | | Hide | Y | | Y | | | Show | Y | | Y | | @@ -214,7 +214,6 @@ An 'X' indicates that the option is not supported by the platform. | Zoom | | | | | | ZoomControlEnabled | | | | | - ### Log To log or not to log? System logger vs custom logger. @@ -223,7 +222,7 @@ To log or not to log? System logger vs custom logger. | Event | Windows | Linux | Mac | Notes | |--------------------------|---------|-------|-----|-------| -| Default Application Menu | Y | Y | Y | | +| Default Application Menu | Y | Y | Y | | ## Tray Menus @@ -320,11 +319,10 @@ TODO: ## Frameless Windows -| Feature | Windows | Linux | Mac | Notes | -|---------|---------|-------|-----|-----------------------------------------------| -| Resize | | | | | -| Drag | | Y | | Linux - can always drag with `Meta`+left mouse | - +| Feature | Windows | Linux | Mac | Notes | +|---------|---------|-------|-----|------------------------------------------------| +| Resize | Y | | Y | | +| Drag | Y | Y | Y | Linux - can always drag with `Meta`+left mouse | ## Mac Specific @@ -359,7 +357,6 @@ TODO: ## Linux Specific - Implementation details for the functions utilized by the `*_linux.go` files are located in the following files: - linux_cgo.go: CGo implementation @@ -367,7 +364,8 @@ Implementation details for the functions utilized by the `*_linux.go` files are ### CGO -By default CGO is utilized to compile the Linux port. This prevents easy cross-compilation and so the PureGo implementation is also being simultaneously developed. +By default CGO is utilized to compile the Linux port. This prevents easy cross-compilation and so the PureGo +implementation is also being simultaneously developed. ### Purego diff --git a/v3/V3 Changes.md b/v3/V3 Changes.md index 57bd28fa9..e3a9830f0 100644 --- a/v3/V3 Changes.md +++ b/v3/V3 Changes.md @@ -14,7 +14,7 @@ Application events are events that are emitted by the application. These events ### Window Events -Window events are events that are emitted by a window. These events include native events such as `WindowDidBecomeMain` on macOS. +Window events are events that are emitted by a window. These events include native events such as `WindowDidBecomeMain` on macOS. Common events are also defined, so they work cross-platform, e.g. `WindowClosing`. ### Custom Events @@ -46,6 +46,8 @@ When emitting an event in JS, it now sends the event to the application. This wi The Window API has largely remained the same, however the methods are now on an instance of a window rather than the runtime. Some notable differences are: - Windows now have a Name that identifies them. This is used to identify the window when emitting events. +- Windows have even more methods on the that were previously unavailable, such as `AbsolutePosition` and `ToggleDevTools`. +- Windows can now accept files via native drag and drop. See the Drag and Drop section for more details. ## ClipBoard diff --git a/v3/examples/plugins/go.mod b/v3/examples/plugins/go.mod index 8f5bb519b..70cdef8a7 100644 --- a/v3/examples/plugins/go.mod +++ b/v3/examples/plugins/go.mod @@ -7,6 +7,7 @@ require github.com/wailsapp/wails/v3 v3.0.0-alpha.0 require ( github.com/bep/debounce v1.2.1 // indirect github.com/dustin/go-humanize v1.0.0 // indirect + github.com/ebitengine/purego v0.4.0-alpha.4 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/google/uuid v1.3.0 // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect @@ -22,7 +23,7 @@ require ( github.com/samber/lo v1.37.0 // indirect github.com/wailsapp/go-webview2 v1.0.2-0.20230604075323-d593c659ca7b // indirect github.com/wailsapp/mimetype v1.4.1 // indirect - github.com/wailsapp/wails/v2 v2.3.2-0.20230117193915-45c3a501d9e6 // indirect + github.com/wailsapp/wails/v2 v2.5.1 // indirect golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/net v0.7.0 // indirect diff --git a/v3/examples/plugins/go.sum b/v3/examples/plugins/go.sum index 68e9723e3..328514ede 100644 --- a/v3/examples/plugins/go.sum +++ b/v3/examples/plugins/go.sum @@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes= +github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= diff --git a/v3/examples/plugins/main.go b/v3/examples/plugins/main.go index d5c0b66c1..0c0d00ad1 100644 --- a/v3/examples/plugins/main.go +++ b/v3/examples/plugins/main.go @@ -39,15 +39,16 @@ func main() { // When true, the original app will be activated when a second instance is launched ActivateAppOnSubsequentLaunch: true, }), - "start_at_login": start_at_login.NewPlugin(), + "start_at_login": start_at_login.NewPlugin(start_at_login.Config{}), }, Assets: application.AssetOptions{ FS: assets, }, }) - window := app.NewWebviewWindow() - window.ToggleDevTools() + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + DevToolsEnabled: true, + }) err := app.Run() diff --git a/v3/pkg/w32/guid.go b/v3/pkg/w32/guid.go new file mode 100644 index 000000000..b6e557f01 --- /dev/null +++ b/v3/pkg/w32/guid.go @@ -0,0 +1,225 @@ +//go:build windows + +package w32 + +// This code has been adapted from: https://github.com/go-ole/go-ole + +/* + +The MIT License (MIT) + +Copyright © 2013-2017 Yasuhiro Matsumoto, + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +const hextable = "0123456789ABCDEF" +const emptyGUID = "{00000000-0000-0000-0000-000000000000}" + +// GUID is Windows API specific GUID type. +// +// This exists to match Windows GUID type for direct passing for COM. +// Format is in xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx. +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4 [8]byte +} + +// NewGUID converts the given string into a globally unique identifier that is +// compliant with the Windows API. +// +// The supplied string may be in any of these formats: +// +// XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +// XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +// {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} +// +// The conversion of the supplied string is not case-sensitive. +func NewGUID(guid string) *GUID { + d := []byte(guid) + var d1, d2, d3, d4a, d4b []byte + + switch len(d) { + case 38: + if d[0] != '{' || d[37] != '}' { + return nil + } + d = d[1:37] + fallthrough + case 36: + if d[8] != '-' || d[13] != '-' || d[18] != '-' || d[23] != '-' { + return nil + } + d1 = d[0:8] + d2 = d[9:13] + d3 = d[14:18] + d4a = d[19:23] + d4b = d[24:36] + case 32: + d1 = d[0:8] + d2 = d[8:12] + d3 = d[12:16] + d4a = d[16:20] + d4b = d[20:32] + default: + return nil + } + + var g GUID + var ok1, ok2, ok3, ok4 bool + g.Data1, ok1 = decodeHexUint32(d1) + g.Data2, ok2 = decodeHexUint16(d2) + g.Data3, ok3 = decodeHexUint16(d3) + g.Data4, ok4 = decodeHexByte64(d4a, d4b) + if ok1 && ok2 && ok3 && ok4 { + return &g + } + return nil +} + +func decodeHexUint32(src []byte) (value uint32, ok bool) { + var b1, b2, b3, b4 byte + var ok1, ok2, ok3, ok4 bool + b1, ok1 = decodeHexByte(src[0], src[1]) + b2, ok2 = decodeHexByte(src[2], src[3]) + b3, ok3 = decodeHexByte(src[4], src[5]) + b4, ok4 = decodeHexByte(src[6], src[7]) + value = (uint32(b1) << 24) | (uint32(b2) << 16) | (uint32(b3) << 8) | uint32(b4) + ok = ok1 && ok2 && ok3 && ok4 + return +} + +func decodeHexUint16(src []byte) (value uint16, ok bool) { + var b1, b2 byte + var ok1, ok2 bool + b1, ok1 = decodeHexByte(src[0], src[1]) + b2, ok2 = decodeHexByte(src[2], src[3]) + value = (uint16(b1) << 8) | uint16(b2) + ok = ok1 && ok2 + return +} + +func decodeHexByte64(s1 []byte, s2 []byte) (value [8]byte, ok bool) { + var ok1, ok2, ok3, ok4, ok5, ok6, ok7, ok8 bool + value[0], ok1 = decodeHexByte(s1[0], s1[1]) + value[1], ok2 = decodeHexByte(s1[2], s1[3]) + value[2], ok3 = decodeHexByte(s2[0], s2[1]) + value[3], ok4 = decodeHexByte(s2[2], s2[3]) + value[4], ok5 = decodeHexByte(s2[4], s2[5]) + value[5], ok6 = decodeHexByte(s2[6], s2[7]) + value[6], ok7 = decodeHexByte(s2[8], s2[9]) + value[7], ok8 = decodeHexByte(s2[10], s2[11]) + ok = ok1 && ok2 && ok3 && ok4 && ok5 && ok6 && ok7 && ok8 + return +} + +func decodeHexByte(c1, c2 byte) (value byte, ok bool) { + var n1, n2 byte + var ok1, ok2 bool + n1, ok1 = decodeHexChar(c1) + n2, ok2 = decodeHexChar(c2) + value = (n1 << 4) | n2 + ok = ok1 && ok2 + return +} + +func decodeHexChar(c byte) (byte, bool) { + switch { + case '0' <= c && c <= '9': + return c - '0', true + case 'a' <= c && c <= 'f': + return c - 'a' + 10, true + case 'A' <= c && c <= 'F': + return c - 'A' + 10, true + } + + return 0, false +} + +// String converts the GUID to string form. It will adhere to this pattern: +// +// {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} +// +// If the GUID is nil, the string representation of an empty GUID is returned: +// +// {00000000-0000-0000-0000-000000000000} +func (guid *GUID) String() string { + if guid == nil { + return emptyGUID + } + + var c [38]byte + c[0] = '{' + putUint32Hex(c[1:9], guid.Data1) + c[9] = '-' + putUint16Hex(c[10:14], guid.Data2) + c[14] = '-' + putUint16Hex(c[15:19], guid.Data3) + c[19] = '-' + putByteHex(c[20:24], guid.Data4[0:2]) + c[24] = '-' + putByteHex(c[25:37], guid.Data4[2:8]) + c[37] = '}' + return string(c[:]) +} + +func putUint32Hex(b []byte, v uint32) { + b[0] = hextable[byte(v>>24)>>4] + b[1] = hextable[byte(v>>24)&0x0f] + b[2] = hextable[byte(v>>16)>>4] + b[3] = hextable[byte(v>>16)&0x0f] + b[4] = hextable[byte(v>>8)>>4] + b[5] = hextable[byte(v>>8)&0x0f] + b[6] = hextable[byte(v)>>4] + b[7] = hextable[byte(v)&0x0f] +} + +func putUint16Hex(b []byte, v uint16) { + b[0] = hextable[byte(v>>8)>>4] + b[1] = hextable[byte(v>>8)&0x0f] + b[2] = hextable[byte(v)>>4] + b[3] = hextable[byte(v)&0x0f] +} + +func putByteHex(dst, src []byte) { + for i := 0; i < len(src); i++ { + dst[i*2] = hextable[src[i]>>4] + dst[i*2+1] = hextable[src[i]&0x0f] + } +} + +// IsEqualGUID compares two GUID. +// +// Not constant time comparison. +func IsEqualGUID(guid1 *GUID, guid2 *GUID) bool { + return guid1.Data1 == guid2.Data1 && + guid1.Data2 == guid2.Data2 && + guid1.Data3 == guid2.Data3 && + guid1.Data4[0] == guid2.Data4[0] && + guid1.Data4[1] == guid2.Data4[1] && + guid1.Data4[2] == guid2.Data4[2] && + guid1.Data4[3] == guid2.Data4[3] && + guid1.Data4[4] == guid2.Data4[4] && + guid1.Data4[5] == guid2.Data4[5] && + guid1.Data4[6] == guid2.Data4[6] && + guid1.Data4[7] == guid2.Data4[7] +} diff --git a/v3/pkg/w32/shell32.go b/v3/pkg/w32/shell32.go index 3b6f3e30f..537356283 100644 --- a/v3/pkg/w32/shell32.go +++ b/v3/pkg/w32/shell32.go @@ -9,6 +9,7 @@ package w32 import ( "errors" "fmt" + "golang.org/x/sys/windows" "syscall" "unsafe" ) @@ -82,6 +83,124 @@ const ( NOTIFYICON_VERSION = 4 ) +var ( + FOLDERID_AccountPictures = NewGUID("{008CA0B1-55B4-4C56-B8A8-4DE4B299D3BE}") + FOLDERID_AddNewPrograms = NewGUID("{DE61D971-5EBC-4F02-A3A9-6C82895E5C04}") + FOLDERID_AdminTools = NewGUID("{724EF170-A42D-4FEF-9F26-B60E846FBA4F}") + FOLDERID_ApplicationShortcuts = NewGUID("{A3918781-E5F2-4890-B3D9-A7E54332328C}") + FOLDERID_AppsFolder = NewGUID("{1E87508D-89C2-42F0-8A7E-645A0F50CA58}") + FOLDERID_AppUpdates = NewGUID("{A305CE99-F527-492B-8B1A-7E76FA98D6E4}") + FOLDERID_CDBurning = NewGUID("{9E52AB10-F80D-49DF-ACB8-4330F5687855}") + FOLDERID_ChangeRemovePrograms = NewGUID("{DF7266AC-9274-4867-8D55-3BD661DE872D}") + FOLDERID_CommonAdminTools = NewGUID("{D0384E7D-BAC3-4797-8F14-CBA229B392B5}") + FOLDERID_CommonOEMLinks = NewGUID("{C1BAE2D0-10DF-4334-BEDD-7AA20B227A9D}") + FOLDERID_CommonPrograms = NewGUID("{0139D44E-6AFE-49F2-8690-3DAFCAE6FFB8}") + FOLDERID_CommonStartMenu = NewGUID("{A4115719-D62E-491D-AA7C-E74B8BE3B067}") + FOLDERID_CommonStartup = NewGUID("{82A5EA35-D9CD-47C5-9629-E15D2F714E6E}") + FOLDERID_CommonTemplates = NewGUID("{B94237E7-57AC-4347-9151-B08C6C32D1F7}") + FOLDERID_ComputerFolder = NewGUID("{0AC0837C-BBF8-452A-850D-79D08E667CA7}") + FOLDERID_ConflictFolder = NewGUID("{4BFEFB45-347D-4006-A5BE-AC0CB0567192}") + FOLDERID_ConnectionsFolder = NewGUID("{6F0CD92B-2E97-45D1-88FF-B0D186B8DEDD}") + FOLDERID_Contacts = NewGUID("{56784854-C6CB-462B-8169-88E350ACB882}") + FOLDERID_ControlPanelFolder = NewGUID("{82A74AEB-AEB4-465C-A014-D097EE346D63}") + FOLDERID_Cookies = NewGUID("{2B0F765D-C0E9-4171-908E-08A611B84FF6}") + FOLDERID_Desktop = NewGUID("{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}") + FOLDERID_DeviceMetadataStore = NewGUID("{5CE4A5E9-E4EB-479D-B89F-130C02886155}") + FOLDERID_Documents = NewGUID("{FDD39AD0-238F-46AF-ADB4-6C85480369C7}") + FOLDERID_DocumentsLibrary = NewGUID("{7B0DB17D-9CD2-4A93-9733-46CC89022E7C}") + FOLDERID_Downloads = NewGUID("{374DE290-123F-4565-9164-39C4925E467B}") + FOLDERID_Favorites = NewGUID("{1777F761-68AD-4D8A-87BD-30B759FA33DD}") + FOLDERID_Fonts = NewGUID("{FD228CB7-AE11-4AE3-864C-16F3910AB8FE}") + FOLDERID_Games = NewGUID("{CAC52C1A-B53D-4EDC-92D7-6B2E8AC19434}") + FOLDERID_GameTasks = NewGUID("{054FAE61-4DD8-4787-80B6-090220C4B700}") + FOLDERID_History = NewGUID("{D9DC8A3B-B784-432E-A781-5A1130A75963}") + FOLDERID_HomeGroup = NewGUID("{52528A6B-B9E3-4ADD-B60D-588C2DBA842D}") + FOLDERID_HomeGroupCurrentUser = NewGUID("{9B74B6A3-0DFD-4F11-9E78-5F7800F2E772}") + FOLDERID_ImplicitAppShortcuts = NewGUID("{BCB5256F-79F6-4CEE-B725-DC34E402FD46}") + FOLDERID_InternetCache = NewGUID("{352481E8-33BE-4251-BA85-6007CAEDCF9D}") + FOLDERID_InternetFolder = NewGUID("{4D9F7874-4E0C-4904-967B-40B0D20C3E4B}") + FOLDERID_Libraries = NewGUID("{1B3EA5DC-B587-4786-B4EF-BD1DC332AEAE}") + FOLDERID_Links = NewGUID("{BFB9D5E0-C6A9-404C-B2B2-AE6DB6AF4968}") + FOLDERID_LocalAppData = NewGUID("{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}") + FOLDERID_LocalAppDataLow = NewGUID("{A520A1A4-1780-4FF6-BD18-167343C5AF16}") + FOLDERID_LocalizedResourcesDir = NewGUID("{2A00375E-224C-49DE-B8D1-440DF7EF3DDC}") + FOLDERID_Music = NewGUID("{4BD8D571-6D19-48D3-BE97-422220080E43}") + FOLDERID_MusicLibrary = NewGUID("{2112AB0A-C86A-4FFE-A368-0DE96E47012E}") + FOLDERID_NetHood = NewGUID("{C5ABBF53-E17F-4121-8900-86626FC2C973}") + FOLDERID_NetworkFolder = NewGUID("{D20BEEC4-5CA8-4905-AE3B-BF251EA09B53}") + FOLDERID_OriginalImages = NewGUID("{2C36C0AA-5812-4B87-BFD0-4CD0DFB19B39}") + FOLDERID_PhotoAlbums = NewGUID("{69D2CF90-FC33-4FB7-9A0C-EBB0F0FCB43C}") + FOLDERID_Pictures = NewGUID("{33E28130-4E1E-4676-835A-98395C3BC3BB}") + FOLDERID_PicturesLibrary = NewGUID("{A990AE9F-A03B-4E80-94BC-9912D7504104}") + FOLDERID_Playlists = NewGUID("{DE92C1C7-837F-4F69-A3BB-86E631204A23}") + FOLDERID_PrintersFolder = NewGUID("{76FC4E2D-D6AD-4519-A663-37BD56068185}") + FOLDERID_PrintHood = NewGUID("{9274BD8D-CFD1-41C3-B35E-B13F55A758F4}") + FOLDERID_Profile = NewGUID("{5E6C858F-0E22-4760-9AFE-EA3317B67173}") + FOLDERID_ProgramData = NewGUID("{62AB5D82-FDC1-4DC3-A9DD-070D1D495D97}") + FOLDERID_ProgramFiles = NewGUID("{905E63B6-C1BF-494E-B29C-65B732D3D21A}") + FOLDERID_ProgramFilesCommon = NewGUID("{F7F1ED05-9F6D-47A2-AAAE-29D317C6F066}") + FOLDERID_ProgramFilesCommonX64 = NewGUID("{6365D5A7-0F0D-45E5-87F6-0DA56B6A4F7D}") + FOLDERID_ProgramFilesCommonX86 = NewGUID("{DE974D24-D9C6-4D3E-BF91-F4455120B917}") + FOLDERID_ProgramFilesX64 = NewGUID("{6D809377-6AF0-444B-8957-A3773F02200E}") + FOLDERID_ProgramFilesX86 = NewGUID("{7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E}") + FOLDERID_Programs = NewGUID("{A77F5D77-2E2B-44C3-A6A2-ABA601054A51}") + FOLDERID_Public = NewGUID("{DFDF76A2-C82A-4D63-906A-5644AC457385}") + FOLDERID_PublicDesktop = NewGUID("{C4AA340D-F20F-4863-AFEF-1F769F2BE730}") + FOLDERID_PublicDocuments = NewGUID("{ED4824AF-DCE4-45A8-81E2-FC7965083634}") + FOLDERID_PublicDownloads = NewGUID("{3D644C9B-1FB8-4F30-9B45-F670235F79C0}") + FOLDERID_PublicGameTasks = NewGUID("{DEBF2536-E1A8-4C59-B6A2-414586476AEA}") + FOLDERID_PublicLibraries = NewGUID("{48DAF80B-E6CF-4F4E-B800-0E69D84EE384}") + FOLDERID_PublicMusic = NewGUID("{3214FAB5-9757-4298-BB61-92A9DEAA44FF}") + FOLDERID_PublicPictures = NewGUID("{B6EBFB86-6907-413C-9AF7-4FC2ABF07CC5}") + FOLDERID_PublicRingtones = NewGUID("{E555AB60-153B-4D17-9F04-A5FE99FC15EC}") + FOLDERID_PublicUserTiles = NewGUID("{0482af6c-08f1-4c34-8c90-e17ec98b1e17}") + FOLDERID_PublicVideos = NewGUID("{2400183A-6185-49FB-A2D8-4A392A602BA3}") + FOLDERID_QuickLaunch = NewGUID("{52a4f021-7b75-48a9-9f6b-4b87a210bc8f}") + FOLDERID_Recent = NewGUID("{AE50C081-EBD2-438A-8655-8A092E34987A}") + FOLDERID_RecordedTVLibrary = NewGUID("{1A6FDBA2-F42D-4358-A798-B74D745926C5}") + FOLDERID_RecycleBinFolder = NewGUID("{B7534046-3ECB-4C18-BE4E-64CD4CB7D6AC}") + FOLDERID_ResourceDir = NewGUID("{8AD10C31-2ADB-4296-A8F7-E4701232C972}") + FOLDERID_Ringtones = NewGUID("{C870044B-F49E-4126-A9C3-B52A1FF411E8}") + FOLDERID_RoamingAppData = NewGUID("{3EB685DB-65F9-4CF6-A03A-E3EF65729F3D}") + FOLDERID_RoamingTiles = NewGUID("{AAA8D5A5-F1D6-4259-BAA8-78E7EF60835E}") + FOLDERID_SampleMusic = NewGUID("{B250C668-F57D-4EE1-A63C-290EE7D1AA1F}") + FOLDERID_SamplePictures = NewGUID("{C4900540-2379-4C75-844B-64E6FAF8716B}") + FOLDERID_SamplePlaylists = NewGUID("{15CA69B3-30EE-49C1-ACE1-6B5EC372AFB5}") + FOLDERID_SampleVideos = NewGUID("{859EAD94-2E85-48AD-A71A-0969CB56A6CD}") + FOLDERID_SavedGames = NewGUID("{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}") + FOLDERID_SavedPictures = NewGUID("{3B193882-D3AD-4EAB-965A-69829D1FB59F}") + FOLDERID_SavedPicturesLibrary = NewGUID("{E25B5812-BE88-4BD9-94B0-29233477B6C3}") + FOLDERID_SavedSearches = NewGUID("{7D1D3A04-DEBB-4115-95CF-2F29DA2920DA}") + FOLDERID_SEARCH_CSC = NewGUID("{ee32e446-31ca-4aba-814f-a5ebd2fd6d5e}") + FOLDERID_SEARCH_MAPI = NewGUID("{98ec0e18-2098-4d44-8644-66979315a281}") + FOLDERID_SearchHome = NewGUID("{190337d1-b8ca-4121-a639-6d472d16972a}") + FOLDERID_SendTo = NewGUID("{8983036C-27C0-404B-8F08-102D10DCFD74}") + FOLDERID_SidebarDefaultParts = NewGUID("{7B396E54-9EC5-4300-BE0A-2482EBAE1A26}") + FOLDERID_SidebarParts = NewGUID("{A75D362E-50FC-4fb7-AC2C-A8BEAA314493}") + FOLDERID_SkyDrive = NewGUID("{A52BBA46-E9E1-435f-B3D9-28DAA648C0F6}") + FOLDERID_SkyDriveCameraRoll = NewGUID("{767E6811-49CB-4273-87C2-20F355E1085B}") + FOLDERID_SkyDriveDocuments = NewGUID("{24D89E24-2F19-4534-9DDE-6A6671FBB8FE}") + FOLDERID_SkyDriveMusic = NewGUID("{C3F2459E-80D6-45DC-BFEF-1F769F2BE730}") + FOLDERID_SkyDrivePictures = NewGUID("{339719B5-8C47-4894-94C2-D8F77ADD44A6}") + FOLDERID_StartMenu = NewGUID("{625B53C3-AB48-4EC1-BA1F-A1EF4146FC19}") + FOLDERID_Startup = NewGUID("{B97D20BB-F46A-4C97-BA10-5E3608430854}") + FOLDERID_SyncManagerFolder = NewGUID("{43668BF8-C14E-49B2-97C9-747784D784B7}") + FOLDERID_SyncResultsFolder = NewGUID("{289a9a43-be44-4057-a41b-587a76d7e7f9}") + FOLDERID_SyncSetupFolder = NewGUID("{0F214138-B1D3-4a90-BBA9-27CBC0C5389A}") + FOLDERID_System = NewGUID("{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}") + FOLDERID_SystemX86 = NewGUID("{D65231B0-B2F1-4857-A4CE-A8E7C6EA7D27}") + FOLDERID_Templates = NewGUID("{A63293E8-664E-48DB-A079-DF759E0509F7}") + FOLDERID_UserPinned = NewGUID("{9E3995AB-1F9C-4F13-B827-48B24B6C7174}") + FOLDERID_UserProfiles = NewGUID("{0762D272-C50A-4BB0-A382-697DCD729B80}") + FOLDERID_UserProgramFiles = NewGUID("{5CD7AEE2-2219-4A67-B85D-6C9CE15660CB}") + FOLDERID_UserProgramFilesCommon = NewGUID("{BCBD3057-CA5C-4622-B42D-BC56DB0AE516}") + FOLDERID_UsersFiles = NewGUID("{F3CE0F7C-4901-4ACC-8648-D5D44B04EF8F}") + FOLDERID_UsersLibraries = NewGUID("{A302545D-DEFF-464b-ABE8-61C8648D939B}") + FOLDERID_Videos = NewGUID("{18989B1D-99B5-455B-841C-AB7C74E4DDFC}") + FOLDERID_VideosLibrary = NewGUID("{491E922F-5643-4AF4-A7EB-4E7A138D8174}") + FOLDERID_Windows = NewGUID("{F38BF404-1D43-42F2-9305-67DE0B28FC23}") +) + var ( modshell32 = syscall.NewLazyDLL("shell32.dll") @@ -95,6 +214,7 @@ var ( procExtractIcon = modshell32.NewProc("ExtractIconW") procGetSpecialFolderPath = modshell32.NewProc("SHGetSpecialFolderPathW") procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW") + procSHGetKnownFolderPath = modshell32.NewProc("SHGetKnownFolderPath") ) func ShellNotifyIcon(cmd uintptr, nid *NOTIFYICONDATA) bool { @@ -108,6 +228,15 @@ func SHBrowseForFolder(bi *BROWSEINFO) uintptr { return ret } +func SHGetKnownFolderPath(rfid *GUID, dwFlags uint32, hToken HANDLE) (string, error) { + var path *uint16 + ret, _, _ := procSHGetKnownFolderPath.Call(uintptr(unsafe.Pointer(rfid)), uintptr(dwFlags), hToken, uintptr(unsafe.Pointer(path))) + if ret != uintptr(windows.S_OK) { + return "", fmt.Errorf("SHGetKnownFolderPath failed: %v", ret) + } + return windows.UTF16PtrToString(path), nil +} + func SHGetPathFromIDList(idl uintptr) string { buf := make([]uint16, 1024) procSHGetPathFromIDList.Call( diff --git a/v3/pkg/w32/typedef.go b/v3/pkg/w32/typedef.go index 6f55f0a26..a60dbd441 100644 --- a/v3/pkg/w32/typedef.go +++ b/v3/pkg/w32/typedef.go @@ -337,14 +337,6 @@ type BROWSEINFO struct { Image int32 } -// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373931.aspx -type GUID struct { - Data1 uint32 - Data2 uint16 - Data3 uint16 - Data4 [8]byte -} - // http://msdn.microsoft.com/en-us/library/windows/desktop/ms221627.aspx type VARIANT struct { VT uint16 // 2 diff --git a/v3/plugins/start_at_login/README.md b/v3/plugins/start_at_login/README.md index b69960d38..b8291a214 100644 --- a/v3/plugins/start_at_login/README.md +++ b/v3/plugins/start_at_login/README.md @@ -24,7 +24,7 @@ You can then call the methods from the frontend: To use this from Go, create a new instance of the plugin, then call the methods on that: ```go - start_at_login := start_at_login.NewPlugin() + start_at_login := start_at_login.NewPlugin(Options) start_at_login.StartAtLogin(true) ``` diff --git a/v3/plugins/start_at_login/plugin.go b/v3/plugins/start_at_login/plugin.go index 44cbae6c6..31b867e87 100644 --- a/v3/plugins/start_at_login/plugin.go +++ b/v3/plugins/start_at_login/plugin.go @@ -7,10 +7,19 @@ import ( type Plugin struct { app *application.App disabled bool + options Config } -func NewPlugin() *Plugin { - return &Plugin{} +type Config struct { + // RegistryKey is the key in the registry to use for storing the start at login setting. + // This defaults to the name of the executable + RegistryKey string +} + +func NewPlugin(options Config) *Plugin { + return &Plugin{ + options: options, + } } // Shutdown is called when the app is shutting down diff --git a/v3/plugins/start_at_login/plugin_linux.go b/v3/plugins/start_at_login/plugin_linux.go index 22d1b244d..7bfbde227 100644 --- a/v3/plugins/start_at_login/plugin_linux.go +++ b/v3/plugins/start_at_login/plugin_linux.go @@ -6,3 +6,11 @@ func (p *Plugin) init() error { // TBD return nil } + +func (p *Plugin) StartAtLogin(enabled bool) error { + panic("not implemented") +} + +func (p *Plugin) IsStartAtLogin() (bool, error) { + panic("not implemented") +} diff --git a/v3/plugins/start_at_login/plugin_window.go b/v3/plugins/start_at_login/plugin_window.go index 7018077ac..1d1545302 100644 --- a/v3/plugins/start_at_login/plugin_window.go +++ b/v3/plugins/start_at_login/plugin_window.go @@ -2,7 +2,87 @@ package start_at_login +import ( + "fmt" + "golang.org/x/sys/windows/registry" + "os" + "path/filepath" + "strings" +) + func (p *Plugin) init() error { // TBD return nil } + +func (p *Plugin) getRegistryKey() (string, string, error) { + exePath, err := os.Executable() + if err != nil { + return "", "", fmt.Errorf("failed to get executable path: %s", err) + } + + registryKey := p.options.RegistryKey + if p.options.RegistryKey == "" { + registryKey = strings.Split(filepath.Base(exePath), ".")[0] + } + + return registryKey, exePath, nil +} + +func openRegKey() (registry.Key, error) { + // Open the registry key + return registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.ALL_ACCESS) +} + +func (p *Plugin) IsStartAtLogin() (bool, error) { + registryKey, exePath, err := p.getRegistryKey() + if err != nil { + return false, err + } + + key, err := openRegKey() + if err != nil { + return false, err + } + defer key.Close() + + // Get the registry value + value, _, err := key.GetStringValue(registryKey) + if err != nil { + return false, nil + } + return value == exePath, nil +} + +func (p *Plugin) StartAtLogin(enabled bool) error { + registryKey, exePath, err := p.getRegistryKey() + if err != nil { + return err + } + + if enabled { + // Open the registry key + key, err := openRegKey() + defer key.Close() + + // Set the registry value + err = key.SetStringValue(registryKey, exePath) + if err != nil { + return fmt.Errorf("failed to set registry value: %s", err) + } + } else { + // Remove registry key + key, err := openRegKey() + if err != nil { + return fmt.Errorf("failed to open registry key: %s", err) + } + defer key.Close() + + // Remove the registry value + err = key.DeleteValue(registryKey) + if err != nil { + return fmt.Errorf("failed to delete registry value: %s", err) + } + } + return nil +}