Changes
8 changed files (+210/-445)
-
-
@@ -42,22 +42,16 @@ - [`pantheon-polkit-agent`](https://archlinux.org/packages/extra/x86_64/pantheon-polkit-agent/) ... The one installed installed using Nix cannot lookup `polkit-agent-helper-1`.## Programs ### `specialisation` ### `hm-clean` `specialisation` activates the given Home Manager specialisation. `hm-clean` removes obsolete Home Manager generations. ```sh # Switch to dark mode specialisation set dark # Switch to light mode specialisation set light # Clean obsolete home-manager generations. hm-clean # Switch to non-specialised profile specialisation unset # Clean obsolete home-manager generations specialisation clean # with verbose logging. hm-clean --verbose ``` ## License
-
-
-
@@ -52,24 +52,12 @@ news.display = "silent";home.stateVersion = "23.11"; specialisation = { dark.configuration = { # One of: "latte", "frappe", "macchiato", "mocha" themes.catppuccin.flavor = module.themes.catppuccin.flavor or "mocha"; }; light.configuration = { themes.catppuccin.flavor = "latte"; }; }; # One of: "latte", "frappe", "macchiato", "mocha" themes.catppuccin.flavor = module.themes.catppuccin.flavor or "mocha"; home.packages = [ pkgs.home-manager (pkgs.callPackage ./programs/specialise { specialisations = pkgs.lib.attrsets.mapAttrsToList (name: _: name) specialisation; }) (pkgs.callPackage ./programs/hm-clean { }) ]; features = nixpkgs.lib.mkDefault {
-
-
-
@@ -0,0 +1,34 @@{ pkgs, installShellFiles, lib, buildGoModule }: buildGoModule rec { name = "hm-clean"; src = ./.; nativeBuildInputs = [ installShellFiles ]; vendorHash = null; ldflags = [ "-X main.homeManagerPath=${pkgs.home-manager}/bin/home-manager" ]; postInstall = '' installShellCompletion --zsh --cmd ${name} <(cat << "EOF" #compdef ${name} function _${name} { local line state _arguments -C \ "--help[Output usage text to stdout]" \ "--verbose[Enable verbose logging]" } if [ "$funcstack[1]" = "_${name}" ]; then _${name} "$@" else compdef _${name} ${name} fi EOF) ''; }
-
-
programs/hm-clean/go.mod (new)
-
@@ -0,0 +1,3 @@module github.com/pocka/system/programs/hm-clean go 1.21.5
-
-
-
@@ -0,0 +1,164 @@package main import ( "errors" "flag" "fmt" "log" "os" "os/exec" "strings" ) // Path to the home-manager bin. // Normally this will be overridden at compile time via "ldflags -X main.homeManagerPath=<path>". var homeManagerPath = "home-manager" func main() { help := flag.Bool("help", false, "Print usage text to stdout.") verbose := flag.Bool("verbose", false, "Enable verbose logging.") flag.Parse() logger := log.New(os.Stderr, "", 0) if *help { fmt.Print(helpText()) os.Exit(0) } clean(logger, *verbose) } func helpText() string { binName := os.Args[0] return fmt.Sprintf(`%s - Clean obsolete Home-Manager generations. [Usage] %s [OPTIONS] --help Print this message to stdout. --verbose Enable verbose logging. `, binName, binName) } func clean(logger *log.Logger, verbose bool) { generations, err := Generations(logger) if err != nil { logger.Fatalf("Failed to get generations: %s", err) } args := []string{"remove-generations"} for i, generation := range generations { if verbose { logger.Printf("Found generation ID=%s", generation.ID) } if i == 0 { if verbose { logger.Printf("Skipping initial generation ID=%s", generation.ID) } continue } logger.Printf("Removing generation ID=%s", generation.ID) args = append(args, generation.ID) } if len(args) == 1 { logger.Print("No generations to remove.") os.Exit(0) } cmd := exec.Command(homeManagerPath, args...) var out strings.Builder cmd.Stdout = &out if err := cmd.Run(); err != nil { logger.Fatalf("Failed to remove generations: %s", err) } } type Generation struct { ID string } func ParseGeneration(line string) (*Generation, error) { tokens := strings.Split(line, " ") colonPosition := -1 for i, token := range tokens { if token == ":" { colonPosition = i break } } if colonPosition < 0 { return nil, errors.New("Unexpected generation output line: No colon found") } if len(tokens) < colonPosition+4 { return nil, errors.New("Unexpected generation output line: Missing tokens") } if tokens[colonPosition+1] != "id" { return nil, fmt.Errorf("Unexpected generation output line: Expected `id`, found `%s`", tokens[colonPosition+1]) } id := tokens[colonPosition+2] if tokens[colonPosition+3] != "->" { return nil, fmt.Errorf("Unexpected generation output line: Expected `->`, found `%s`", tokens[colonPosition+3]) } return &Generation{ ID: id, }, nil } func Generations(logger *log.Logger) ([]Generation, error) { cmd := exec.Command(homeManagerPath, "generations") var out strings.Builder cmd.Stdout = &out if err := cmd.Run(); err != nil { return nil, err } stdout := out.String() lines := strings.Split(stdout, "\n") generations := make([]Generation, 0, len(lines)) for _, line := range strings.Split(stdout, "\n") { if len(strings.TrimSpace(line)) == 0 { continue } tokens := strings.Split(line, " ") if len(tokens) == 0 { continue } generation, err := ParseGeneration(line) if err != nil { logger.Printf("Failed to parse generation output: %s", err.Error()) continue } generations = append(generations, *generation) } return generations, nil }
-
-
programs/specialise/default.nix (deleted)
-
@@ -1,68 +0,0 @@{ pkgs, installShellFiles, lib, buildGoModule, specialisations ? null }: buildGoModule rec { name = "specialise"; src = ./.; nativeBuildInputs = [ installShellFiles ]; vendorHash = null; ldflags = [ "-X main.homeManagerPath=${pkgs.home-manager}/bin/home-manager" ]; postInstall = '' installShellCompletion --zsh --cmd ${name} <(cat << "EOF" #compdef ${name} function _${name} { local line state _arguments -C \ "--help[Output usage text to stdout]" \ "--verbose[Enable verbose logging]" \ "1: :->cmds" \ "*::arg:->args" case "$state" in (cmds) _values "specialise command" \ "set[Switch to a specialisation]" \ "unset[Switch to a normal generation]" \ "clean[Delete obsolete generations]" ;; (args) case $line[1] in (set) _${name}_set ;; esac ;; esac } function _${name}_set { local state _arguments -C \ "1: :->cmds" case "$state" in (cmds) _values "specialisations" \ ${if specialisations == null then "" else lib.strings.concatStringsSep " " specialisations } ;; esac } if [ "$funcstack[1]" = "_${name}" ]; then _${name} "$@" else compdef _${name} ${name} fi EOF) ''; }
-
-
programs/specialise/go.mod (deleted)
-
@@ -1,3 +0,0 @@module github.com/pocka/system/programs/specialise go 1.21.5
-
-
programs/specialise/specialise.go (deleted)
-
@@ -1,347 +0,0 @@package main import ( "errors" "flag" "fmt" "io/fs" "log" "os" "os/exec" "strings" ) // Path to the home-manager bin. // Normally this will be overridden at compile time via "ldflags -X main.homeManagerPath=<path>". var homeManagerPath = "home-manager" func main() { help := flag.Bool("help", false, "Print usage text to stdout.") verbose := flag.Bool("verbose", false, "Enable verbose logging.") flag.Parse() logger := log.New(os.Stderr, "", 0) if *help { fmt.Print(helpText()) os.Exit(0) } args := flag.Args() if len(args) < 1 { fmt.Fprint(os.Stderr, helpText()) logger.Fatal("No command provided.") } switch args[0] { case "set": if len(args) != 2 { fmt.Fprint(os.Stderr, helpText()) logger.Fatal("`set` requires exact 1 argument") } setSpecialisation(args[1], *verbose, logger) case "unset": activateNonSpecialisedProfile(logger) case "clean": clean(logger) default: fmt.Fprint(os.Stderr, helpText()) logger.Fatalf("Unknown command: %s", args[0]) } } func helpText() string { binName := os.Args[0] return fmt.Sprintf(`%s - Manage Home-Manager specialisation easily. [Usage] %s set <specialisation> %s unset [COMMANDS] set <specialisation> Activate the particular specialisation. unset Activate the most recent home-manager profile which have specialisations. clean Delete generations other than the latest generation and the latest generation having specialisation directory. [OPTIONS] --help Print this message to stdout. --verbose Enable verbose logging. `, binName, binName, binName) } func setSpecialisation(specialisation string, verbose bool, logger *log.Logger) { if strings.Contains(specialisation, "/") { logger.Fatalf("specialisation cannot have slash (\"/\"): %s", specialisation) } generations, err := Generations(logger) if err != nil { logger.Fatalf("Failed to get generations: %s", err) } for _, generation := range generations { specialisations, err := generation.Specialisations() if err != nil { logger.Fatalf("Failed to get specialisations for %s: %s", generation.ID, err) } if verbose { logger.Printf("Found generation with ID=%s", generation.ID) } for _, s := range specialisations { if verbose { logger.Printf("Found specialisation with Name=%s in generation ID=%s", s.Name, generation.ID) } if s.Name != specialisation { continue } logger.Printf("Activate %s from ID=%s", specialisation, generation.ID) if err := s.Profile.Activate(); err != nil { logger.Fatalf("Failed to activate %s: %s", specialisation, err) } logger.Printf("Activated %s", specialisation) os.Exit(0) } } logger.Fatalf("No specialisation named \"%s\" found", specialisation) } func activateNonSpecialisedProfile(logger *log.Logger) { generations, err := Generations(logger) if err != nil { logger.Fatalf("Failed to get generations: %s", err) } for _, generation := range generations { specialisations, err := generation.Specialisations() if err != nil { logger.Fatalf("Failed to check specialisations (ID=%s): %s", generation.ID, err) } if len(specialisations) == 0 { continue } logger.Printf("Activate generation (ID=%s)", generation.ID) if err := generation.Profile.Activate(); err != nil { logger.Fatalf("Failed to activate: %s", err) } os.Exit(0) } logger.Fatal("No generation having specialisations found: Manually switch to a profile using home-manager.") } func clean(logger *log.Logger) { generations, err := Generations(logger) if err != nil { logger.Fatalf("Failed to get generations: %s", err) } foundGenerationHavingSpecialisation := false args := []string{"remove-generations"} for i, generation := range generations { if !foundGenerationHavingSpecialisation { specialisations, err := generation.Specialisations() if err != nil { logger.Fatalf("Failed to get specialisations for ID=%s: %s", generation.ID, err) } if len(specialisations) > 0 { foundGenerationHavingSpecialisation = true continue } } if i == 0 { continue } logger.Printf("Removing generation ID=%s", generation.ID) args = append(args, generation.ID) } if len(args) == 1 { logger.Print("Nothing to clean.") os.Exit(0) } cmd := exec.Command(homeManagerPath, args...) var out strings.Builder cmd.Stdout = &out if err := cmd.Run(); err != nil { logger.Fatalf("Failed to remove generations: %s", err) } } type Profile struct { Path string } func (p Profile) Activate() error { dir := os.DirFS(p.Path) bin, err := dir.Open("activate") if err != nil { return err } defer bin.Close() stat, err := bin.Stat() if err != nil { return err } if stat.IsDir() { return fmt.Errorf("Cannot activate %s: target is directory", p.Path) } path := fmt.Sprintf("%s/activate", p.Path) cmd := exec.Command(path) return cmd.Run() } type Specialisation struct { Name string Profile Profile } type Generation struct { ID string Profile Profile } func ParseGeneration(line string) (*Generation, error) { tokens := strings.Split(line, " ") colonPosition := -1 for i, token := range tokens { if token == ":" { colonPosition = i break } } if colonPosition < 0 { return nil, errors.New("Unexpected generation output line: No colon found") } if len(tokens) < colonPosition+4 { return nil, errors.New("Unexpected generation output line: Missing tokens") } if tokens[colonPosition+1] != "id" { return nil, fmt.Errorf("Unexpected generation output line: Expected `id`, found `%s`", tokens[colonPosition+1]) } id := tokens[colonPosition+2] if tokens[colonPosition+3] != "->" { return nil, fmt.Errorf("Unexpected generation output line: Expected `->`, found `%s`", tokens[colonPosition+3]) } path := strings.Join(tokens[(colonPosition+4):], " ") return &Generation{ ID: id, Profile: Profile{ Path: path, }, }, nil } func Generations(logger *log.Logger) ([]Generation, error) { cmd := exec.Command(homeManagerPath, "generations") var out strings.Builder cmd.Stdout = &out if err := cmd.Run(); err != nil { return nil, err } stdout := out.String() lines := strings.Split(stdout, "\n") generations := make([]Generation, 0, len(lines)) for _, line := range strings.Split(stdout, "\n") { if len(strings.TrimSpace(line)) == 0 { continue } tokens := strings.Split(line, " ") if len(tokens) == 0 { continue } generation, err := ParseGeneration(line) if err != nil { logger.Printf("Failed to parse generation output: %s", err.Error()) continue } generations = append(generations, *generation) } return generations, nil } func (g Generation) Specialisations() ([]Specialisation, error) { dir := os.DirFS(g.Profile.Path) entries, err := fs.ReadDir(dir, "specialisation") if err != nil { if errors.Is(err, fs.ErrNotExist) { return []Specialisation{}, nil } return nil, err } specialisations := make([]Specialisation, 0, len(entries)) for _, entry := range entries { name := entry.Name() specialisations = append(specialisations, Specialisation{ Name: name, Profile: Profile{ Path: fmt.Sprintf("%s/specialisation/%s", g.Profile.Path, name), }, }) } return specialisations, nil }
-