❄ Personal NixOS Flake Manager
nixos home-manager go nix
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 440 lines 8.7 kB view raw
1package main 2 3import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "os/exec" 8 "time" 9 10 "github.com/urfave/cli/v2" 11) 12 13type Configuration struct { 14 Notify bool `json:"notify"` 15 Editor string `json:"editor"` 16 Flake string `json:"flake"` 17 Notifier string `json:"notifier"` 18 AllowUnfree bool `json:"allow-unfree"` 19 AllowInsecure bool `json:"allow-insecure"` 20 ExtraArgs []string `json:"extra-args"` 21} 22 23type ActionDetails struct { 24 Name string 25 Verb string 26 UsableWithNH bool 27} 28 29var configuration Configuration 30 31const ( 32 Switch = iota 33 Boot 34 Test 35 Build 36 DryActivate 37 BuildVM 38 Instantiate 39 Generations 40 Packages 41) 42 43func init() { 44 configurationPath := os.Getenv("RUI_CONFIG") 45 46 if configurationPath == "" { 47 configurationPath = os.Getenv("XDG_CONFIG_HOME") + "/rui/config.json" 48 } 49 50 content, err := os.ReadFile(configurationPath) 51 52 if err != nil { 53 return 54 } 55 56 if err := json.Unmarshal(content, &configuration); err != nil { 57 return 58 } 59} 60 61func subcommand(action int, aliases []string, flags []cli.Flag, commandAction func(c *cli.Context, action int) error) *cli.Command { 62 return &cli.Command{ 63 Name: actionName(action), 64 Aliases: aliases, 65 Flags: flags, 66 Action: func(c *cli.Context) error { 67 return commandAction(c, action) 68 }, 69 } 70} 71 72func main() { 73 homeFlags := []cli.Flag{ 74 &cli.BoolFlag{ 75 Name: "use-home-manager", 76 }, 77 &cli.StringFlag{ 78 Name: "user", 79 }, 80 } 81 osFlags := []cli.Flag{ 82 &cli.BoolFlag{ 83 Name: "use-nixos-rebuild", 84 }, 85 &cli.StringFlag{ 86 Name: "hostname", 87 }, 88 } 89 90 if err := (&cli.App{ 91 Name: "rui", 92 Usage: "Personal NixOS Flake Manager", 93 Description: "Personal NixOS Flake Manager", 94 EnableBashCompletion: true, 95 Authors: []*cli.Author{ 96 { 97 Name: "Fuwn", 98 Email: "contact@fuwn.me", 99 }, 100 }, 101 Copyright: fmt.Sprintf("Copyright (c) 2024-%s Fuwn", fmt.Sprint(time.Now().Year())), 102 ExitErrHandler: func(c *cli.Context, err error) { 103 if err != nil { 104 fmt.Println(err) 105 os.Exit(1) 106 } 107 }, 108 Suggest: true, 109 Flags: []cli.Flag{ 110 &cli.BoolFlag{ 111 Name: "allow-unfree", 112 Action: func(c *cli.Context, b bool) error { 113 state := "0" 114 115 if b { 116 state = "1" 117 } 118 119 return os.Setenv("NIXPKGS_ALLOW_UNFREE", state) 120 }, 121 }, 122 &cli.BoolFlag{ 123 Name: "allow-insecure", 124 Action: func(c *cli.Context, b bool) error { 125 state := "0" 126 127 if b { 128 state = "1" 129 } 130 131 return os.Setenv("NIXPKGS_ALLOW_INSECURE", state) 132 }, 133 }, 134 }, 135 Before: func(c *cli.Context) error { 136 if configuration.AllowUnfree { 137 c.Set("allow-unfree", "1") 138 } 139 140 if configuration.AllowInsecure { 141 c.Set("allow-insecure", "1") 142 } 143 144 return nil 145 }, 146 Commands: []*cli.Command{ 147 { 148 Name: "hs", 149 Action: func(c *cli.Context) error { 150 return c.App.Command("home").Command("switch").Run(c) 151 }, 152 Hidden: true, 153 Description: "Alias for `home switch`", 154 }, 155 { 156 Name: "osw", 157 Action: func(c *cli.Context) error { 158 return c.App.Command("os").Command("switch").Run(c) 159 }, 160 Hidden: true, 161 Usage: "Alias for `os switch`", 162 }, 163 { 164 Name: "home", 165 Subcommands: []*cli.Command{ 166 subcommand(Switch, []string{"sw"}, homeFlags, home), 167 subcommand(Build, []string{}, homeFlags, home), 168 subcommand(Instantiate, []string{}, homeFlags, home), 169 subcommand(Generations, []string{"gens"}, homeFlags, home), 170 subcommand(Packages, []string{"pkgs"}, homeFlags, home), 171 { 172 Name: "news", 173 Flags: []cli.Flag{ 174 &cli.StringFlag{ 175 Name: "user", 176 }, 177 }, 178 Action: func(c *cli.Context) error { 179 flake := configuration.Flake 180 extraArgs := c.Args().Slice() 181 182 extraArgs = append(extraArgs, configuration.ExtraArgs...) 183 184 if flake == "" { 185 flake = os.Getenv("FLAKE") 186 } 187 188 if user := c.String("user"); user != "" { 189 flake = fmt.Sprintf("%s#%s", flake, user) 190 } 191 192 return command("home-manager", append([]string{"news", "--flake", 193 flake}, extraArgs...)...) 194 }, 195 }, 196 }, 197 }, 198 { 199 Name: "os", 200 Subcommands: []*cli.Command{ 201 subcommand(Switch, []string{"sw"}, osFlags, ruiOS), 202 subcommand(Boot, []string{}, osFlags, ruiOS), 203 subcommand(Test, []string{}, osFlags, ruiOS), 204 subcommand(Build, []string{}, osFlags, ruiOS), 205 subcommand(DryActivate, []string{"dry"}, osFlags, ruiOS), 206 subcommand(BuildVM, []string{"vm"}, osFlags, ruiOS), 207 }, 208 }, 209 { 210 Name: "edit", 211 Action: func(c *cli.Context) error { 212 var found bool 213 214 editor := configuration.Editor 215 flake := configuration.Flake 216 217 if flake == "" { 218 flake = os.Getenv("FLAKE") 219 } 220 221 if editor == "" { 222 if editor, found = os.LookupEnv("FLAKE_EDITOR"); !found { 223 editor = os.Getenv("EDITOR") 224 } 225 } 226 227 return command(editor, flake) 228 }, 229 }, 230 }, 231 }).Run(os.Args); err != nil { 232 fmt.Println(err) 233 os.Exit(1) 234 } 235} 236 237func command(name string, args ...string) error { 238 cmd := exec.Command(name, args...) 239 cmd.Stdin = os.Stdin 240 cmd.Stdout = os.Stdout 241 cmd.Stderr = os.Stderr 242 243 return cmd.Run() 244} 245 246func notify(message string) error { 247 if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" { 248 return nil 249 } 250 251 notifier := configuration.Notifier 252 253 if notifier == "" { 254 notifier = "notify-send" 255 } 256 257 notifySend, err := exec.LookPath(notifier) 258 259 if err != nil { 260 return nil 261 } 262 263 if configuration.Notify { 264 return command(notifySend, "Rui", message) 265 } 266 267 return nil 268} 269 270func actionName(action int) string { 271 name := "switch" 272 273 switch action { 274 case Boot: 275 name = "boot" 276 277 break 278 279 case Test: 280 name = "test" 281 282 break 283 284 case Build: 285 name = "build" 286 287 break 288 289 case DryActivate: 290 name = "dry-activate" 291 292 break 293 294 case BuildVM: 295 name = "build-vm" 296 297 break 298 299 case Instantiate: 300 name = "instantiate" 301 302 break 303 304 case Generations: 305 name = "generations" 306 307 break 308 309 case Packages: 310 name = "packages" 311 312 break 313 } 314 315 return name 316} 317 318func actionDetails(action int) (string, string, bool) { 319 switch action { 320 case Switch: 321 return actionName(action), "switched", true 322 323 case Boot: 324 return actionName(action), "booted", false 325 326 case Test: 327 return actionName(action), "tested", false 328 329 case Build: 330 return actionName(action), "built", true 331 332 case DryActivate: 333 return actionName(action), "dry activated", false 334 335 case BuildVM: 336 return actionName(action), "VM built", false 337 338 case Instantiate: 339 return actionName(action), "instantiated", false 340 341 case Generations: 342 return actionName(action), "generations listed", false 343 344 case Packages: 345 return actionName(action), "packages shown", false 346 } 347 348 return "", "", false 349} 350 351func home(c *cli.Context, action int) error { 352 nh, err := exec.LookPath("nh") 353 extraArgs := c.Args().Slice() 354 name, verb, usableWithNH := actionDetails(action) 355 356 extraArgs = append(extraArgs, configuration.ExtraArgs...) 357 358 if err := notify("Queued home " + name); err != nil { 359 return err 360 } 361 362 if err == nil && !c.Bool("use-home-manager") { 363 if !usableWithNH { 364 return fmt.Errorf("This command is not supported with nh. Use --use-home-manager to use Home Manager instead.") 365 } 366 367 err = command(nh, append([]string{"home", name, "--"}, 368 extraArgs...)...) 369 } else { 370 user := c.String("user") 371 372 if user == "" { 373 user = os.Getenv("USER") 374 } 375 376 flake := configuration.Flake 377 378 if flake == "" { 379 flake = os.Getenv("FLAKE") 380 } 381 382 err = command("home-manager", append([]string{name, 383 "--flake", fmt.Sprintf("%s#%s", flake, user)}, 384 extraArgs...)...) 385 } 386 387 if err != nil { 388 return notify(fmt.Sprintf("Failed to %s home: %s", name, err.Error())) 389 } 390 391 return notify("Home " + verb) 392} 393 394func ruiOS(c *cli.Context, action int) error { 395 nh, err := exec.LookPath("nh") 396 name, verb, usableWithNH := actionDetails(action) 397 398 if err := notify("Queued OS " + name); err != nil { 399 return err 400 } 401 402 if err == nil && !c.Bool("use-nixos-rebuild") { 403 if !usableWithNH { 404 return fmt.Errorf("This command is not supported with nh. Use --use-nixos-rebuild to use nixos-rebuild instead.") 405 } 406 407 err = command(nh, "os", name) 408 } else { 409 escalator := "sudo" 410 411 if doas, err := exec.LookPath("doas"); err != nil { 412 escalator = doas 413 } 414 415 hostname := c.String("hostname") 416 417 if hostname == "" { 418 hostname, err = os.Hostname() 419 420 if err != nil { 421 return err 422 } 423 } 424 425 flake := configuration.Flake 426 427 if flake == "" { 428 flake = os.Getenv("FLAKE") 429 } 430 431 err = command(escalator, "nixos-rebuild", name, "--flake", 432 fmt.Sprintf("%s#%s", flake, hostname)) 433 } 434 435 if err != nil { 436 return notify(fmt.Sprintf("Failed to %s OS: %s", name, err.Error())) 437 } 438 439 return notify("OS " + verb) 440}