├── main.go ├── go.mod ├── .travis.yml ├── tap_template ├── .gitignore ├── version.go ├── go.sum ├── update_tap.sh ├── LICENSE.md ├── cli.go ├── profiles.go ├── fallback.go ├── ui.go ├── README.md └── install.go /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | var header = "Shadowfox updater " + tag 8 | 9 | func main() { 10 | if len(os.Args) > 1 { 11 | cli() 12 | } else { 13 | if err := createUI(); err != nil { 14 | createFallbackUI() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/SrKomodo/shadowfox-updater 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/gen2brain/dlgs v0.0.0-20180629122906-342edb4c68c1 7 | github.com/go-ini/ini v1.42.0 8 | github.com/mitchellh/go-homedir v1.1.0 9 | github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e 10 | ) 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: "1.12" 3 | 4 | script: 5 | - ./build.sh 6 | 7 | deploy: 8 | - provider: releases 9 | api_key: $GITHUB_TOKEN 10 | file_glob: true 11 | file: dist/* 12 | skip_cleanup: true 13 | on: 14 | tags: true 15 | - provider: script 16 | script: ./update_tap.sh $TAP_TOKEN 17 | on: 18 | tags: true 19 | -------------------------------------------------------------------------------- /tap_template: -------------------------------------------------------------------------------- 1 | class ShadowfoxUpdater < Formula 2 | desc "An automatic updater for ShadowFox" 3 | homepage "https://github.com/SrKomodo/shadowfox-updater" 4 | url "https://github.com/SrKomodo/shadowfox-updater/releases/download/tag/shadowfox_mac_x64" 5 | version "tag" 6 | sha256 "checksum" 7 | 8 | def install 9 | File.rename("shadowfox_mac_x64", "shadowfox") 10 | bin.install "shadowfox" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.exe~ 24 | *.test 25 | *.prof 26 | 27 | .vscode 28 | shadowfox-updater 29 | dist 30 | vendor 31 | homebrew-tap 32 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "regexp" 7 | ) 8 | 9 | var tag string 10 | 11 | func checkForUpdate() (bool, string, error) { 12 | resp, err := http.Get("https://api.github.com/repos/SrKomodo/shadowfox-updater/releases/latest") 13 | if err != nil { 14 | return false, "", err 15 | } 16 | 17 | defer resp.Body.Close() 18 | data, err := ioutil.ReadAll(resp.Body) 19 | if err != nil { 20 | return false, "", err 21 | } 22 | 23 | regex := regexp.MustCompile(`"tag_name":"(.+?)"`) 24 | newTag := string(regex.FindSubmatch(data)[1]) 25 | 26 | return newTag != tag, newTag, nil 27 | } 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gen2brain/dlgs v0.0.0-20180629122906-342edb4c68c1 h1:16o0LcrCHuKADRSOTYxoXTRQpdVo9BDeABauof+9Em8= 2 | github.com/gen2brain/dlgs v0.0.0-20180629122906-342edb4c68c1/go.mod h1:/eFcjDXaU2THSOOqLxOPETIbHETnamk8FA/hMjhg/gU= 3 | github.com/go-ini/ini v1.42.0 h1:TWr1wGj35+UiWHlBA8er89seFXxzwFn11spilrrj+38= 4 | github.com/go-ini/ini v1.42.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 5 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 6 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 7 | github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e h1:VAzdS5Nw68fbf5RZ8RDVlUvPXNU6Z3jtPCK/qvm4FoQ= 8 | github.com/skratchdot/open-golang v0.0.0-20190402232053-79abb63cd66e/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 9 | -------------------------------------------------------------------------------- /update_tap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Make sure we are on the right directory 4 | cd $(dirname $0) 5 | pwd 6 | 7 | TAG=$(git describe) 8 | CHECKSUM=$(sha256sum dist/shadowfox_mac_x64 | cut -d' ' -f1) 9 | echo Tag=$TAG 10 | echo Checksum=$CHECKSUM 11 | 12 | # Clone repo 13 | git clone https://github.com/SrKomodo/homebrew-tap.git 14 | cd homebrew-tap 15 | pwd 16 | git remote rm origin 17 | git remote add origin https://SrKomodo:$1@github.com/SrKomodo/homebrew-tap.git 18 | 19 | # Compile template 20 | IFS='' 21 | while read line; do 22 | REPLACE1=${line//tag/$TAG} 23 | REPLACE2=${REPLACE1//checksum/$CHECKSUM} 24 | echo $REPLACE2 25 | done < ../tap_template > Formula/shadowfox-updater.rb 26 | 27 | # Push to tap 28 | git add Formula/shadowfox-updater.rb 29 | git commit -m $TAG 30 | git push --set-upstream origin master -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | func cli() { 9 | paths, names, err := getProfilePaths() 10 | if err != nil { 11 | fmt.Println(err.Error()) 12 | fmt.Scanln() 13 | panic(err) 14 | } 15 | 16 | version := flag.Bool("version", false, "Shows the current version") 17 | uninstalling := flag.Bool("uninstall", false, "Wheter to install or uninstall ShadowFox") 18 | profileName := flag.String("profile-name", "", "Name of profile to use, if not defined or not found will fallback to profile-index") 19 | profileIndex := flag.Int("profile-index", 0, "Index of profile to use") 20 | uuids := flag.Bool("generate-uuids", false, "Wheter to automatically generate UUIDs or not") 21 | theme := flag.Bool("set-dark-theme", false, "Wheter to automatically set Firefox's dark theme") 22 | 23 | flag.Parse() 24 | 25 | if *version { 26 | fmt.Println(header) 27 | return 28 | } 29 | 30 | var path string 31 | for i, name := range names { 32 | if name == *profileName { 33 | path = paths[i] 34 | } 35 | } 36 | if path == "" { 37 | path = paths[*profileIndex] 38 | } 39 | 40 | if *uninstalling { 41 | err := uninstall(path) 42 | if err != nil { 43 | fmt.Println(err.Error()) 44 | fmt.Scanln() 45 | panic(err) 46 | } 47 | } else { 48 | err := install(path, *uuids, *theme) 49 | if err != nil { 50 | fmt.Println(err.Error()) 51 | fmt.Scanln() 52 | panic(err) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /profiles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/go-ini/ini" 10 | homedir "github.com/mitchellh/go-homedir" 11 | ) 12 | 13 | func getProfilePaths() ([]string, []string, error) { 14 | // iniPaths stores all profiles.ini files we have to check 15 | iniPaths := []string{} 16 | 17 | // Get the home directory 18 | homedir, err := homedir.Dir() 19 | if err != nil { 20 | return nil, nil, fmt.Errorf("Couldn't find home directory: %s", err) 21 | } 22 | 23 | // Possible places where we should check for profiles.ini 24 | possible := []string{ 25 | "./profiles.ini", 26 | homedir + "\\AppData\\Roaming\\Mozilla\\Firefox\\profiles.ini", 27 | homedir + "/Library/Application Support/Firefox/profiles.ini", 28 | homedir + "/.mozilla/firefox/profiles.ini", 29 | homedir + "/.mozilla/firefox-trunk/profiles.ini", 30 | } 31 | 32 | // Check if profiles.ini exists on each possible path and add them to the list 33 | for _, p := range possible { 34 | _, err := os.Stat(p) 35 | if os.IsNotExist(err) { 36 | continue 37 | } 38 | if err != nil { 39 | return nil, nil, fmt.Errorf("Couldn't check if %s exists: %s", p, err) 40 | } 41 | iniPaths = append(iniPaths, p) 42 | } 43 | 44 | // If we didnt find anything then we just give up 45 | if len(iniPaths) == 0 { 46 | return nil, nil, errors.New("Couldn't find any profiles") 47 | } 48 | 49 | var paths []string 50 | var names []string 51 | 52 | // For each possible ini file 53 | for _, p := range iniPaths { 54 | file, err := ini.Load(p) 55 | if err != nil { 56 | return nil, nil, fmt.Errorf("Could not read profiles.ini, make sure its encoded in UTF-8: %s", err) 57 | } 58 | 59 | // Find the Path key and add it to the list 60 | for _, section := range file.Sections() { 61 | if key, err := section.GetKey("Path"); err == nil { 62 | path := key.String() 63 | isRelative := section.Key("IsRelative").MustInt(1) 64 | 65 | if isRelative == 1 { 66 | paths = append(paths, filepath.Join(filepath.Dir(p), path)) 67 | } else { 68 | paths = append(paths, path) 69 | } 70 | names = append(names, filepath.Base(path)) 71 | } 72 | } 73 | } 74 | 75 | return paths, names, nil 76 | } 77 | -------------------------------------------------------------------------------- /fallback.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/skratchdot/open-golang/open" 8 | ) 9 | 10 | func createFallbackUI() { 11 | fmt.Println(header) 12 | 13 | shouldUpdate, newTag, err := checkForUpdate() 14 | if err != nil { 15 | fmt.Println(err.Error()) 16 | fmt.Scanln() 17 | panic(err) 18 | } 19 | 20 | if shouldUpdate { 21 | var choice string 22 | fmt.Printf("There is a new shadowfox-updater version available (%s -> %s)\nDo you want to update? [y/n]", tag, newTag) 23 | fmt.Scanln(&choice) 24 | wantToUpdate := (choice == "y" || choice == "Y") 25 | if wantToUpdate { 26 | open.Start("https://github.com/SrKomodo/shadowfox-updater/#installing") 27 | } 28 | } 29 | 30 | var choice string 31 | paths, names, err := getProfilePaths() 32 | if err != nil { 33 | fmt.Println(err.Error()) 34 | fmt.Scanln() 35 | panic(err) 36 | } 37 | 38 | if paths == nil { 39 | fmt.Println("ShadowFox couldn't automatically find 'profiles.ini'. Please follow these steps:") 40 | fmt.Println(" 1. Close the program") 41 | fmt.Println(" 2. Move the program to the folder 'profiles.ini' is located") 42 | fmt.Println(" 3. Run the program") 43 | fmt.Scanln() 44 | return 45 | } 46 | 47 | fmt.Println("Available profiles:") 48 | for i, name := range names { 49 | fmt.Printf(" %d: %s\n", i, name) 50 | } 51 | 52 | fmt.Printf("\nWhich one would you like to use? [%d-%d] ", 0, len(names)-1) 53 | 54 | var profile string 55 | for { 56 | fmt.Scanln(&choice) 57 | i, err := strconv.Atoi(choice) 58 | if err != nil || i < 0 || i > len(paths) { 59 | fmt.Print("Please input a valid number ") 60 | } else { 61 | profile = paths[i] 62 | break 63 | } 64 | } 65 | 66 | fmt.Print("\nDo you want to (1) install or (2) uninstall ShadowFox? [1/2] ") 67 | fmt.Scanln(&choice) 68 | 69 | if choice == "2" { 70 | uninstall(profile) 71 | fmt.Print("\nShadowFox was successfully uninstalled! (Press 'enter' to exit)") 72 | fmt.Scanln() 73 | return 74 | } 75 | 76 | fmt.Print("\nWould you like to auto-generate UUIDs? [y/n] ") 77 | fmt.Scanln(&choice) 78 | uuids := (choice == "y" || choice == "Y") 79 | 80 | fmt.Print("\nWould you like to automatically set the Firefox dark theme? [y/n] ") 81 | fmt.Scanln(&choice) 82 | theme := (choice == "y" || choice == "Y") 83 | 84 | err = install(profile, uuids, theme) 85 | if err != nil { 86 | fmt.Println(err.Error()) 87 | fmt.Scanln() 88 | panic(err) 89 | } 90 | 91 | fmt.Print("\nShadowFox was successfully installed! (Press 'enter' to exit)") 92 | fmt.Scanln() 93 | return 94 | } 95 | -------------------------------------------------------------------------------- /ui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gen2brain/dlgs" 7 | "github.com/skratchdot/open-golang/open" 8 | ) 9 | 10 | func createUI() error { 11 | shouldUpdate, newTag, err := checkForUpdate() 12 | if err != nil { 13 | _, err := dlgs.Error(header, err.Error()) 14 | if err != nil { 15 | return err 16 | } 17 | } 18 | 19 | if shouldUpdate { 20 | wantToUpdate, err := dlgs.Question(header, fmt.Sprintf("There is a new shadowfox-updater version available (%s -> %s)\nDo you want to update?", tag, newTag), true) 21 | if err != nil { 22 | return err 23 | } 24 | if wantToUpdate { 25 | open.Start("https://github.com/SrKomodo/shadowfox-updater/#installing") 26 | return nil 27 | } 28 | } 29 | 30 | paths, names, err := getProfilePaths() 31 | if err != nil { 32 | _, err := dlgs.Error(header, err.Error()) 33 | if err != nil { 34 | return err 35 | } 36 | } 37 | 38 | name, selected, err := dlgs.List(header, "Which Firefox profile are you going to use?", names) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | if !selected { 44 | _, err := dlgs.Info(header, "You didn't pick any profile, the application will now close.") 45 | return err 46 | } 47 | 48 | pathIndex := 0 49 | for _, name2 := range names { 50 | if name == name2 { 51 | break 52 | } 53 | pathIndex++ 54 | } 55 | profilePath := paths[pathIndex] 56 | 57 | action, selected, err := dlgs.List(header, "What do you want to do?", []string{"Install/Update Shadowfox", "Uninstall Shadowfox"}) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | if !selected { 63 | _, err := dlgs.Info(header, "You didn't pick any action, the application will now close.") 64 | return err 65 | } 66 | 67 | if action == "Install/Update Shadowfox" { 68 | shouldGenerateUUIDs, err := dlgs.Question(header, "Would you like to auto-generate UUIDs?", true) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | shouldSetTheme, err := dlgs.Question(header, "Would you like to automatically set the Firefox dark theme?", false) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | err = install(profilePath, shouldGenerateUUIDs, shouldSetTheme) 79 | if err == nil { 80 | _, err = dlgs.Info(header, "Shadowfox has been succesfully installed!") 81 | if err != nil { 82 | return err 83 | } 84 | } else { 85 | _, err := dlgs.Error(header, err.Error()) 86 | if err != nil { 87 | return err 88 | } 89 | } 90 | } else { 91 | err := uninstall(profilePath) 92 | if err == nil { 93 | _, err = dlgs.Info(header, "Shadowfox has been succesfully uninstalled!") 94 | if err != nil { 95 | return err 96 | } 97 | } else { 98 | _, err := dlgs.Error(header, err.Error()) 99 | if err != nil { 100 | return err 101 | } 102 | } 103 | } 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shadowfox Updater 2 | 3 | This is a cross-platform installer/uninstaller/updater for [Shadowfox](https://github.com/overdodactyl/ShadowFox), a universal dark theme for Firefox. 4 | 5 | ## Installing 6 | 7 | - For all platforms: go to the [latest release](https://github.com/SrKomodo/shadowfox-updater/releases/latest) and download the respective file for your OS 8 | - If you are in Linux or Mac, you will probably need to run `chmod +x [filename]` for the OS to register it as an executable 9 | - On Arch Linux you can install the package `shadowfox-updater` from AUR 10 | - On MacOS, you can install with either [Homebrew](https://brew.sh/) or [MacPorts.](https://www.macports.org/) 11 | 12 | - Homebrew installation: 13 | 14 | ``` 15 | $ brew install srkomodo/tap/shadowfox-updater 16 | $ shadowfox 17 | ``` 18 | 19 | - MacPorts installation: 20 | 21 | ``` 22 | $ sudo port install shadowfox-updater 23 | $ shadowfox-updater 24 | ``` 25 | 26 | ## How to use 27 | 28 | There are various ways to use Shadowfox Updater 29 | 30 | ### GUI Mode 31 | 32 | If you run the file, it should open a series of prompts that will ask you everything needed to install or uninstall Shadowfox. 33 | 34 | The "Profile to use" list will let you choose which profile you are going to work with. 35 | 36 | Then, a prompt will give you the option to either install/update or uninstall Shadowfox. 37 | 38 | The "Auto-Generate UUIDs" prompt, if accepted, will make the updater automatically populate the `internal_UUIDs.txt` file, which is used for styling of extensions. Generally you would toggle this unless you want to manage precisely which extensions get styled. 39 | 40 | The "Set Firefox dark theme" prompt, if accepted, will make the updater automatically enable Firefox's dark theme for it's UI and devtools. If you already have the dark theme enabled, you shouldn't toggle this one. 41 | 42 | #### Fallback 43 | 44 | If the graphical UI fails to load, the program will load a more basic text-only prompt that has the same features as the usual UI but without the fancy graphical interface. 45 | 46 | ### CLI Mode 47 | 48 | If you run the file with one or more arguments, the updater will work as a command line tool, which can be useful for automated scripts and such. Instead of explaining how it works I'm just going to paste the result of the command `shadowfox-updater -h`. 49 | 50 | ``` 51 | Usage of shadowfox-updater: 52 | -generate-uuids 53 | Wheter to automatically generate UUIDs or not 54 | -profile-index int 55 | Index of profile to use 56 | -profile-name string 57 | Name of profile to use, if not defined or not found will fallback to profile-index 58 | -set-dark-theme 59 | Wheter to automatically set Firefox's dark theme 60 | -uninstall 61 | Wheter to install or uninstall ShadowFox 62 | ``` 63 | 64 | ## Common issues 65 | 66 | ### ShadowFox couldn't automatically find 'profiles.ini' 67 | 68 | If this error shows up then your Firefox installation is probably located in a non-standard location. In this case, the solution would be to move the shadowfox executable to wherever `profiles.ini` is located. 69 | 70 | 1. Open Firefox and go to `about:profiles` 71 | 2. Click "Open root folder" 72 | 3. Go back a few folders until you see `profiles.ini` 73 | 4. Copy the updater executable to where `profiles.ini` is located 74 | 5. Run the updater again 75 | 76 | ### Couldn't read prefs.js: no such file or directory 77 | 78 | This issue can happen if the profile you are trying to install to hasn't ever been opened. It can be easily fixed by just running Firefox with that profile and then running the updater again. 79 | 80 | ### panic: key-value delimiter not found 81 | 82 | This issue usually happens because `profiles.ini` is encoded in some encoding different from UTF-8, this can be easily fixed by changing `profiles.ini`'s encoding to UTF-8 with your favorite text editor or command line tool of choice. 83 | -------------------------------------------------------------------------------- /install.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | func downloadFile(file string) (string, error) { 15 | resp, err := http.Get("https://raw.githubusercontent.com/overdodactyl/ShadowFox/master/" + file) 16 | if err != nil { 17 | return "", err 18 | } 19 | defer resp.Body.Close() 20 | 21 | data, err := ioutil.ReadAll(resp.Body) 22 | if err != nil { 23 | return "", err 24 | } 25 | 26 | return string(data), nil 27 | } 28 | 29 | func backUp(path string) error { 30 | _, err := os.Stat(path) 31 | if os.IsNotExist(err) { 32 | return nil 33 | } 34 | if err != nil { 35 | return err 36 | } 37 | 38 | err = os.Rename(path, path+time.Now().Format(".2006-01-02-15-04-05.backup")) 39 | if err != nil { 40 | return err 41 | } 42 | return nil 43 | } 44 | 45 | func readFile(path string) (string, error) { 46 | _, err := os.Stat(path) 47 | if os.IsNotExist(err) { 48 | err := ioutil.WriteFile(path, nil, 0644) 49 | if err != nil { 50 | return "", err 51 | } 52 | return "", nil 53 | } 54 | if err != nil { 55 | return "", err 56 | } 57 | 58 | bytes, err := ioutil.ReadFile(path) 59 | if err != nil { 60 | return "", err 61 | } 62 | return string(bytes), nil 63 | } 64 | 65 | func uninstall(profile string) error { 66 | for _, file := range []string{ 67 | "userChrome.css", 68 | "userContent.css", 69 | } { 70 | path := filepath.Join(profile, "chrome", file) 71 | if err := backUp(path); err != nil { 72 | return fmt.Errorf("Couldn't backup %s: %s", file, err) 73 | } 74 | } 75 | return nil 76 | } 77 | 78 | func install(profilePath string, generateUUIDs bool, setTheme bool) error { 79 | chromePath := filepath.Join(profilePath, "chrome") 80 | customPath := filepath.Join(chromePath, "ShadowFox_customization") 81 | 82 | if err := os.MkdirAll(customPath, 0700); err != nil { 83 | return fmt.Errorf("Couldn't create folders: %s", err) 84 | } 85 | 86 | colors, err := readFile(filepath.Join(customPath, "colorOverrides.css")) 87 | if err != nil { 88 | return fmt.Errorf("Couldn't read colorOverrides.css: %s", err) 89 | } 90 | 91 | if generateUUIDs { 92 | err := backUp(filepath.Join(customPath, "internal_UUIDs.txt")) 93 | if err != nil { 94 | return fmt.Errorf("Couldn't backup internal_UUIDs.txt: %s", err) 95 | } 96 | 97 | prefs, err := readFile(filepath.Join(profilePath, "prefs.js")) 98 | if err != nil { 99 | return fmt.Errorf("Couldn't read prefs.js: %s", err) 100 | } 101 | 102 | regex := regexp.MustCompile(`\\\"(.+?)\\\":\\\"(.{8}-.{4}-.{4}-.{4}-.{12})\\\"`) 103 | matches := regex.FindAllStringSubmatch(prefs, -1) 104 | output := "" 105 | for _, match := range matches { 106 | output += match[1] + "=" + match[2] + "\n" 107 | } 108 | 109 | if err := ioutil.WriteFile(filepath.Join(customPath, "internal_UUIDs.txt"), []byte(output), 0644); err != nil { 110 | return fmt.Errorf("Couldn't write internal_UUIDs.txt: %s", err) 111 | } 112 | } 113 | 114 | uuidBytes, err := readFile(filepath.Join(customPath, "internal_UUIDs.txt")) 115 | if err != nil { 116 | return fmt.Errorf("Couldn't read internal_UUIDs.txt: %s", err) 117 | } 118 | uuids := string(uuidBytes) 119 | pairs := regexp.MustCompile("(.+)=(.+)").FindAllStringSubmatch(uuids, -1) 120 | 121 | for _, file := range []string{ 122 | "userChrome", 123 | "userContent", 124 | } { 125 | path := filepath.Join(chromePath, file) 126 | 127 | if err := backUp(path + ".css"); err != nil { 128 | return fmt.Errorf("Couldn't backup %s: %s", file, err) 129 | } 130 | 131 | contents, err := downloadFile(file + ".css") 132 | if err != nil { 133 | return fmt.Errorf("Couldn't download %s: %s", file, err) 134 | } 135 | 136 | // Add color overrides 137 | startI := strings.Index(contents, "--start-indicator-for-updater-scripts: black;") 138 | endI := strings.Index(contents, "--end-indicator-for-updater-scripts: black;") + 43 139 | contents = contents[:startI] + colors + contents[endI:] 140 | 141 | // Add customizations 142 | custom, err := readFile(filepath.Join(customPath, file+"_customization.css")) 143 | if err != nil { 144 | return fmt.Errorf("Couldn't read %s_customization.css: %s", file, err) 145 | } 146 | contents = contents + string(custom) 147 | 148 | // Add UUIDs 149 | for _, key := range pairs { 150 | contents = strings.Replace(contents, key[1], key[2], -1) 151 | } 152 | 153 | // Write file 154 | if err := ioutil.WriteFile(path+".css", []byte(contents), 0644); err != nil { 155 | return fmt.Errorf("Couldn't write %s: %s", file, err) 156 | } 157 | } 158 | 159 | // Set dark theme 160 | if setTheme { 161 | path := filepath.Join(profilePath, "prefs.js") 162 | prefsContent, err := readFile(path) 163 | if err != nil { 164 | return fmt.Errorf("Couldn't read prefs.js: %s", err) 165 | } 166 | 167 | for key, value := range map[string]string{ 168 | "lightweightThemes.selectedThemeID": "\"firefox-compact-dark@mozilla.org\"", 169 | "browser.uidensity": "1", 170 | "devtools.theme": "\"dark\"", 171 | } { 172 | regex := regexp.MustCompile("user_pref(\"" + key + "\", .+);") 173 | replace := "user_pref(\"" + key + "\", " + value + ");" 174 | if regex.MatchString(prefsContent) { 175 | prefsContent = regex.ReplaceAllString(prefsContent, replace) 176 | } else { 177 | prefsContent += replace + "\n" 178 | } 179 | } 180 | 181 | if err := ioutil.WriteFile(path, []byte(prefsContent), 0644); err != nil { 182 | return fmt.Errorf("Couldn't write prefs.js: %s", err) 183 | } 184 | } 185 | 186 | return nil 187 | } 188 | --------------------------------------------------------------------------------