Files
gosleep/cmd_init.go
T
forust d1cc207ce4 feat: init command, README, fix config creation, move cicd
- cmd_init.go: interactive setup wizard (gosleep-timer init)
- README.md: comprehensive documentation
- config/config.go: fix os.IsNotExist → errors.Is for wrapped err
- main.go: remove '(no TUI)' from run help
- cicd.yaml moved to .github/workflows/cicd.yaml
2026-07-03 09:52:44 +02:00

264 lines
7.1 KiB
Go

package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"github.com/spf13/cobra"
"github.com/forust/gosleep-timer/config"
)
var initCmd = &cobra.Command{
Use: "init",
Short: "Interactive config setup wizard",
Long: `Creates a new config.yaml with interactive prompts.
Walks through all modules and profiles step by step.`,
RunE: func(cmd *cobra.Command, args []string) error {
cfgPath, err := getConfigPath()
if err != nil {
return err
}
// Check if config already exists
if _, err := os.Stat(cfgPath); err == nil {
fmt.Printf("Config already exists at %s\n", cfgPath)
fmt.Print("Overwrite? [y/N]: ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Println("Aborted.")
return nil
}
}
cfg := runWizard()
if cfg == nil {
fmt.Println("\nSetup aborted.")
return nil
}
if err := config.Save(cfgPath, cfg); err != nil {
return fmt.Errorf("save config: %w", err)
}
fmt.Printf("\n✅ Config saved to %s\n", cfgPath)
fmt.Println("Run 'gosleep-timer' to start the TUI.")
return nil
},
}
func init() {
rootCmd.AddCommand(initCmd)
}
func runWizard() *config.Config {
reader := bufio.NewReader(os.Stdin)
cfg := config.DefaultConfig()
p := &config.Profile{}
fmt.Println(strings.Repeat("=", 50))
fmt.Println(" ⏱ gosleep-timer Setup Wizard")
fmt.Println(strings.Repeat("=", 50))
// Profile name
fmt.Print("\nProfile name [default]: ")
name := readLine(reader, "default")
// Duration
fmt.Print("Default duration (e.g. 25m, 1h, 90s) [25m]: ")
dur := readLine(reader, "25m")
// Workspace module
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Workspace — switch to a workspace on timer start")
wsEnabled := askYesNo(reader, "Enable workspace switch?", true)
ws := &config.WorkspaceConfig{Enabled: wsEnabled}
if wsEnabled {
fmt.Print("Workspace number [12]: ")
wsStr := readLine(reader, "12")
ws.Workspace, _ = strconv.Atoi(wsStr)
if ws.Workspace == 0 {
ws.Workspace = 12
}
fmt.Println("Backend (auto-detect / niri / hyprland / kde):")
fmt.Print(" [auto]: ")
ws.Backend = readLine(reader, "auto")
if ws.Backend == "auto" {
ws.Backend = ""
}
}
// Media module
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Media — control music player on timer start")
mediaEnabled := askYesNo(reader, "Enable media control?", true)
media := &config.MediaConfig{Enabled: mediaEnabled}
if mediaEnabled {
fmt.Println("Player action (stop / pause / play-pause / next / previous / none):")
fmt.Print(" [stop]: ")
action := readLine(reader, "stop")
media.Action = action
}
// Lock module
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Lock — lock screen when timer finishes")
lockEnabled := askYesNo(reader, "Enable screen lock?", true)
lock := &config.LockConfig{Enabled: lockEnabled}
if lockEnabled {
fmt.Print("Custom lock command (leave empty for auto): ")
cmd := readLine(reader, "")
lock.Command = cmd
}
// Kill module
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Kill — terminate apps when timer starts")
killEnabled := askYesNo(reader, "Enable process killer?", false)
kill := &config.KillConfig{Enabled: killEnabled}
if killEnabled {
fmt.Print("Processes to kill (comma separated, e.g. firefox,telegram): ")
procs := readLine(reader, "")
if procs != "" {
for _, p := range strings.Split(procs, ",") {
p = strings.TrimSpace(p)
if p != "" {
kill.Processes = append(kill.Processes, p)
}
}
}
}
// Notify module
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Notify — desktop notification when timer finishes")
notifyEnabled := askYesNo(reader, "Enable notifications?", true)
notify := &config.NotifyConfig{Enabled: notifyEnabled}
if notifyEnabled {
fmt.Print("Notification sound path (empty = no sound): ")
notify.Sound = readLine(reader, "")
}
// Sound module
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Sound — play audio when timer finishes")
soundEnabled := askYesNo(reader, "Enable sound?", true)
sound := &config.SoundConfig{Enabled: soundEnabled}
if soundEnabled {
sound.File = "/usr/share/sounds/freedesktop/stereo/complete.oga"
fmt.Printf(" Default: %s\n", sound.File)
fmt.Print("Custom sound file (leave empty for default): ")
custom := readLine(reader, "")
if custom != "" {
sound.File = custom
}
}
// Brightness module
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Brightness — dim screen during timer")
brightnessEnabled := askYesNo(reader, "Enable brightness control?", false)
brightness := &config.BrightnessConfig{Enabled: brightnessEnabled}
if brightnessEnabled {
fmt.Print("Brightness level 0-100 [0]: ")
valStr := readLine(reader, "0")
val, _ := strconv.Atoi(valStr)
brightness.Value = val
}
// Mute module
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Mute — mute audio during timer")
muteEnabled := askYesNo(reader, "Enable mute?", false)
mute := &config.MuteConfig{Enabled: muteEnabled}
// Custom commands
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Custom — run your own commands")
customEnabled := askYesNo(reader, "Enable custom commands?", false)
custom := &config.CustomConfig{Enabled: customEnabled}
if customEnabled {
fmt.Println("Pre-commands (run BEFORE timer, one per line, empty line to end):")
custom.Pre = readLines(reader, "pre")
fmt.Println("Post-commands (run AFTER timer, one per line, empty line to end):")
custom.Post = readLines(reader, "post")
}
// Script module
fmt.Println(strings.Repeat("-", 50))
fmt.Println("Script — run a script with timer context (args: pre/post, profile, duration)")
scriptEnabled := askYesNo(reader, "Enable script module?", false)
script := &config.ScriptConfig{Enabled: scriptEnabled}
if scriptEnabled {
fmt.Print("Script path: ")
script.Path = readLine(reader, "")
}
// Build final config
p.Modules = &config.Modules{
Workspace: ws,
Media: media,
Lock: lock,
Kill: kill,
Notify: notify,
Sound: sound,
Brightness: brightness,
Mute: mute,
Custom: custom,
Script: script,
}
cfg.Profiles = map[string]config.Profile{
name: *p,
}
fmt.Println(strings.Repeat("=", 50))
fmt.Println(" ✅ Setup complete!")
fmt.Printf(" Profile: %s\n", name)
fmt.Printf(" Duration: %s\n", dur)
fmt.Println(strings.Repeat("=", 50))
return cfg
}
func readLine(reader *bufio.Reader, fallback string) string {
line, _ := reader.ReadString('\n')
line = strings.TrimSpace(line)
if line == "" {
return fallback
}
return line
}
func askYesNo(reader *bufio.Reader, prompt string, defaultYes bool) bool {
suffix := "[Y/n]"
if !defaultYes {
suffix = "[y/N]"
}
fmt.Printf("%s %s: ", prompt, suffix)
answer := strings.ToLower(strings.TrimSpace(readLine(reader, "")))
if answer == "" {
return defaultYes
}
return answer == "y" || answer == "yes"
}
func readLines(reader *bufio.Reader, label string) []string {
var lines []string
for {
fmt.Printf(" %s> ", label)
line, _ := reader.ReadString('\n')
line = strings.TrimSpace(line)
if line == "" {
break
}
lines = append(lines, line)
}
return lines
}