This commit is contained in:
gabrielkheisa 2025-04-19 20:05:02 +07:00
commit 56503c065d
11 changed files with 372 additions and 0 deletions

46
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Build & Release ThemeChanger
on:
push:
branches:
- main
jobs:
release:
name: Build and Upload Release ZIP
runs-on: windows-latest
permissions:
contents: write # Needed for creating/updating releases
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Tidy dependencies
run: go mod tidy
- name: Build executable
run: go build -ldflags="-H windowsgui" -o themechanger.exe
- name: Create ZIP with icon and binary
run: Compress-Archive -Path themechanger.exe,icon.ico -DestinationPath themechanger.zip
- name: Create or update release
uses: softprops/action-gh-release@v2
with:
tag_name: latest
name: Latest Build
files: themechanger.zip
body: |
✅ Automatic build from latest commit to `main`
🛠️ Includes `themechanger.exe` and `icon.ico`
🔄 Ready to drop into `shell:startup`
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
# Go build artifacts
*.exe
*.exe~
*.out
*.test
# Go module files
go.sum
# IDE/editor files
.vscode/
.idea/
*.swp
*.swo
# Windows files
Thumbs.db
Desktop.ini
# Logs
*.log
# Archive files
*.zip
*.tar
*.tar.gz
# Systray tray icon (you may want to ignore it if it's generated)
# icon.ico
# GitHub Actions artifacts (if manually downloaded)
*.7z
*.rar
# Build folders (if any)
bin/
dist/

126
README.md Normal file
View File

@ -0,0 +1,126 @@
# Auto Theme + Wallpaper Switcher (Windows)
This is a lightweight Windows tray app that **automatically switches between light and dark mode** based on the time of day (6 AM to 6 PM), and optionally changes your desktop wallpaper. It also silently restarts `explorer.exe` after theme changes to force a full UI refresh (taskbar, Start menu, etc.).
![cover](/screenshot/cover.jpg)
Runs quietly in the background with a tray icon and auto-starts on login (via `shell:startup`).
---
## Features
- Automatically switches between **light** and **dark** mode
- Optionally changes wallpaper to match the theme
- Gracefully skips if wallpaper is missing (no crashes)
- Runs silently with a **tray icon** and no console window
- Uses native Windows APIs (registry + wallpaper)
- Automatically **restarts `explorer.exe`** for full theme refresh
---
## Setup Instructions
Place these files in the same directory:
```
themechanger.exe ← compiled binary
icon.ico ← tray icon (required)
light.jpg/png/bmp/... ← optional day wallpaper
dark.jpg/png/bmp/... ← optional night wallpaper
```
Supported image formats: `.jpg`, `.jpeg`, `.png`, `.bmp`, `.webp`
The app will look for `light.*` and `dark.*` in its own folder. If no image is found, it simply skips the wallpaper change.
---
## Auto-Start on Login
To launch the app automatically when you log in:
1. Press `Win + R`
2. Type: `shell:startup` and press Enter
3. **Create a shortcut to `themechanger.exe`:**
* Right-click on `themechanger.exe`.
* Select "Create shortcut".
4. **Move the shortcut you just created into the Startup folder.** The Startup folder is the one that opened in step 2.
Done! It will now auto-start silently every time.
> No need to edit the registry or install anything!
---
## Build It Yourself
If you'd like to compile from source:
### 1. Install Go
[https://go.dev/dl](https://go.dev/dl)
### 2. Initialize your Go module
```bash
go mod init themechanger
go mod tidy
```
### 3. Build the executable
```bash
go build -ldflags="-H windowsgui" -o themechanger.exe
```
This produces a GUI app (no console window).
---
## Technical Details
- Uses Go and the [`systray`](https://github.com/getlantern/systray) library for tray UI
- Theme switching via registry keys:
- `AppsUseLightTheme`
- `SystemUsesLightTheme`
- Wallpaper changes with `SystemParametersInfoW` API
- Explorer restart via:
```bash
taskkill /f /im explorer.exe && start explorer.exe
```
---
## 🖼 Tray Preview
You must provide your own `icon.ico` in the same folder — this will be used as the tray icon.
> Tip: You can create `.ico` files from PNGs using online converters like [icoconvert.com](https://icoconvert.com)
---
## Notes
- You can manually quit the app from the tray menu
- Time check runs every 60 seconds
- Changing the clock or timezone will trigger a switch automatically
- No admin rights needed
---
## Optional Ideas
- Add GUI to configure start/stop time
- Manual override options in tray menu
- Geo-based sunrise/sunset switching
---
## License
MIT — free for personal and commercial use.
---
Made with Go, Windows quirks, and coffee ☕

BIN
dark.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

19
go.mod Normal file
View File

@ -0,0 +1,19 @@
module themechanger
go 1.23.1
require (
github.com/getlantern/systray v1.2.2
golang.org/x/sys v0.32.0
)
require (
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
)

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
light.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB

144
main.go Normal file
View File

@ -0,0 +1,144 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"unsafe"
"github.com/getlantern/systray"
"golang.org/x/sys/windows/registry"
)
// Set Windows dark/light mode via registry
func setMode(light bool) {
key, _ := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.SET_VALUE)
defer key.Close()
val := uint32(0)
if light {
val = 1
}
key.SetDWordValue("AppsUseLightTheme", val)
key.SetDWordValue("SystemUsesLightTheme", val)
}
// Restart Explorer to fully apply theme
func restartExplorer() {
exec.Command("taskkill", "/f", "/im", "explorer.exe").Run()
time.Sleep(1 * time.Second)
exec.Command("explorer.exe").Start()
}
// Get current theme mode from registry
func getCurrentMode() string {
key, _ := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE)
defer key.Close()
val, _, _ := key.GetIntegerValue("AppsUseLightTheme")
if val == 1 {
return "light"
}
return "dark"
}
// Find image in same dir with any supported extension
func findImageFile(name string) string {
exePath, _ := os.Executable()
dir := filepath.Dir(exePath)
matches, _ := filepath.Glob(filepath.Join(dir, name+".*"))
for _, f := range matches {
ext := strings.ToLower(filepath.Ext(f))
if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp" || ext == ".webp" {
return f
}
}
return ""
}
// Get current wallpaper path from registry
func getCurrentWallpaper() string {
key, _ := registry.OpenKey(registry.CURRENT_USER, `Control Panel\Desktop`, registry.QUERY_VALUE)
defer key.Close()
val, _, _ := key.GetStringValue("Wallpaper")
return val
}
// Set desktop wallpaper via Windows API
func setWallpaper(path string) {
user32 := syscall.NewLazyDLL("user32.dll")
sysParam := user32.NewProc("SystemParametersInfoW")
p, _ := syscall.UTF16PtrFromString(path)
sysParam.Call(0x0014, 0, uintptr(unsafe.Pointer(p)), uintptr(0x01|0x02))
}
// Main background loop
func backgroundLoop() {
lightImg := findImageFile("light")
darkImg := findImageFile("dark")
lastMode := getCurrentMode()
for {
hour := time.Now().Hour()
newMode := "dark"
var targetImg string
if hour >= 6 && hour < 18 {
newMode = "light"
targetImg = lightImg
} else {
targetImg = darkImg
}
if newMode != lastMode {
setMode(newMode == "light")
restartExplorer()
lastMode = newMode
}
if targetImg != "" && getCurrentWallpaper() != targetImg {
setWallpaper(targetImg)
}
time.Sleep(60 * time.Second)
}
}
// Load icon from local directory (icon.ico)
func getIconBytes() []byte {
exePath, _ := os.Executable()
iconPath := filepath.Join(filepath.Dir(exePath), "icon.ico")
data, err := ioutil.ReadFile(iconPath)
if err != nil {
fmt.Println("icon.ico not found or unreadable.")
return nil
}
return data
}
// Systray startup
func onReady() {
icon := getIconBytes()
if icon != nil {
systray.SetIcon(icon)
}
systray.SetTitle("Auto Theme")
systray.SetTooltip("Auto Theme + Wallpaper Switcher")
mQuit := systray.AddMenuItem("Quit", "Exit the program")
go func() {
go backgroundLoop()
<-mQuit.ClickedCh
systray.Quit()
}()
}
func onExit() {}
func main() {
systray.Run(onReady, onExit)
}

BIN
screenshot/cover.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
screenshot/dark_ss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 711 KiB

BIN
screenshot/light_ss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 KiB