├── .gitignore ├── quickshell ├── icons │ ├── confused.gif │ ├── pacman.svg │ ├── mannu.svg │ ├── arch.svg │ ├── ii.svg │ ├── noctalia.svg │ └── dms.svg └── shell.qml ├── main.go ├── example ├── keybinds │ ├── unbinds.conf │ ├── dms.conf │ ├── noctalia.conf │ └── caelestia.conf ├── config.json └── README.md ├── go.mod ├── utils ├── help.go ├── state.go ├── config.go ├── autofix.go └── actions.go ├── cmd ├── panel.go ├── current.go ├── autofix.go ├── expSetup.go ├── reload.go ├── switchKeybinds.go ├── list.go ├── apply.go └── root.go ├── cmake_uninstall.cmake.in ├── go.sum ├── .github └── workflows │ └── update-completions.yml ├── AUR └── PKGBUILD ├── CONTRIBUTING.md ├── man └── qswitch.1 ├── CMakeLists.txt ├── README.md ├── completions ├── qswitch.zsh ├── qswitch.fish └── qswitch.bash └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .qmlls.ini 3 | .archive 4 | qswitch 5 | AUR/qswitch* 6 | AUR/pkg 7 | -------------------------------------------------------------------------------- /quickshell/icons/confused.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MannuVilasara/qswitch/HEAD/quickshell/icons/confused.gif -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Manpreet Singh 3 | */ 4 | package main 5 | 6 | import "qswitch/cmd" 7 | 8 | func main() { 9 | cmd.Execute() 10 | } 11 | -------------------------------------------------------------------------------- /example/keybinds/unbinds.conf: -------------------------------------------------------------------------------- 1 | unbind=Ctrl+Super, T 2 | unbind=Super, Super_L 3 | unbind=Super, Super_R 4 | unbind=Super, I 5 | unbind=Super, V 6 | unbind=Super, Period 7 | unbind=Super, C 8 | unbind=Super, X 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module qswitch 2 | 3 | go 1.25.4 4 | 5 | require ( 6 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 7 | github.com/spf13/cobra v1.10.2 // indirect 8 | github.com/spf13/pflag v1.0.9 // indirect 9 | ) 10 | -------------------------------------------------------------------------------- /example/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flavours": ["ii", "noctalia-shell", "caelestia", "dms"], 3 | "keybinds": { 4 | "caelestia": "caelestia.conf", 5 | "ii": "default", 6 | "noctalia-shell": "noctalia.conf", 7 | "dms": "dms.conf" 8 | }, 9 | "unbinds": true, 10 | "panel_keybind": "Super+Alt, P" 11 | } 12 | -------------------------------------------------------------------------------- /quickshell/icons/pacman.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /utils/help.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func ShowSetupMessage() { 8 | fmt.Println(` 9 | ⚠️ qswitch Setup Required 10 | 11 | This appears to be your first time running qswitch. 12 | 13 | This tool requires proper setup to work correctly. 14 | please run qswitch exp-setup to set it up. 15 | 16 | After setup, you can run qswitch normally. 17 | 18 | To bypass this message (not recommended), use the --itrustmyself flag.`) 19 | } 20 | -------------------------------------------------------------------------------- /cmd/panel.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Manpreet Singh 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "qswitch/utils" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // panelCmd represents the panel command 13 | var panelCmd = &cobra.Command{ 14 | Use: "panel", 15 | Short: "Toggle the panel", 16 | Long: `Toggle the QuickSwitchPanel on or off.`, 17 | Run: func(cmd *cobra.Command, args []string) { 18 | utils.TogglePanel() 19 | }, 20 | } 21 | 22 | func init() { 23 | rootCmd.AddCommand(panelCmd) 24 | } 25 | -------------------------------------------------------------------------------- /cmd/current.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Manpreet Singh 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | 9 | "qswitch/utils" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // currentCmd represents the current command 15 | var currentCmd = &cobra.Command{ 16 | Use: "current", 17 | Short: "Show current flavour", 18 | Long: `Display the currently active flavour.`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | fmt.Println(utils.ReadState()) 21 | }, 22 | } 23 | 24 | func init() { 25 | rootCmd.AddCommand(currentCmd) 26 | } 27 | -------------------------------------------------------------------------------- /example/keybinds/dms.conf: -------------------------------------------------------------------------------- 1 | 2 | 3 | unbind = Super+Shift, N 4 | bind = Super, Tab, exec, dms ipc call spotlight toggle 5 | bind = Super, N, exec, dms ipc call widget toggle controlCenterButton 6 | bind = Super, A, exec, dms ipc call widget toggle clock 7 | bind = Super+Shift, N, exec, dms ipc call notepad toggle 8 | 9 | bind = Ctrl+Super, T, exec, dms ipc call dankdash wallpaper 10 | bind = Super, V, exec, dms ipc call clipboard toggle 11 | bind = Super, J, exec, dms ipc call bar toggle index 0 12 | bind = Super, L, exec, $dms ipc call lock lock 13 | bind = Super, I, exec, dms ipc call settings toggle 14 | 15 | -------------------------------------------------------------------------------- /cmd/autofix.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Manpreet Singh 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "qswitch/utils" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // autofixCmd represents the autofix command 13 | var autofixCmd = &cobra.Command{ 14 | Use: "autofix", 15 | Short: "Autofix known issues", 16 | Long: `This command attempts to automatically fix known issues 17 | that may arise during the usage of the application.`, 18 | Run: func(cmd *cobra.Command, args []string) { 19 | utils.ApplyAutofix() 20 | }, 21 | } 22 | 23 | func init() { 24 | rootCmd.AddCommand(autofixCmd) 25 | } 26 | -------------------------------------------------------------------------------- /example/keybinds/noctalia.conf: -------------------------------------------------------------------------------- 1 | 2 | #### NOCTALIA BINDS #### 3 | 4 | env = noct, qs -c noctalia-shell ipc call 5 | 6 | bind = Super, Tab, exec, $noct launcher toggle 7 | 8 | bind = Super, A, exec, $noct controlCenter toggle 9 | bind = Super, I, exec, $noct settings toggle 10 | bind = Super, V, exec, pkill fuzzel || $noct launcher clipboard 11 | bind = Super, Period, exec, pkill fuzzel || $noct launcher emoji 12 | bind = Super, N, exec, $noct calendar toggle 13 | bindl = Super, C, exec , $noct launcher calculator 14 | bind = Super+Alt, Delete, exec, $noct sessionMenu toggle 15 | bind = Super, L, exec, $noct lockScreen lock 16 | bind = Super, J, exec, $noct bar toggle 17 | bind = Ctrl+Super, T, exec, $noct wallpaper toggle 18 | -------------------------------------------------------------------------------- /cmd/expSetup.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Manpreet Singh 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "qswitch/utils" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // expSetupCmd represents the expSetup command 13 | var expSetupCmd = &cobra.Command{ 14 | Use: "exp-setup", 15 | Short: "Experimental setup", 16 | Long: `Perform the initial setup for qswitch.`, 17 | Run: func(cmd *cobra.Command, args []string) { 18 | config := utils.LoadConfig() 19 | force, _ := cmd.Flags().GetBool("force") 20 | utils.Setup(config, force) 21 | }, 22 | } 23 | 24 | func init() { 25 | rootCmd.AddCommand(expSetupCmd) 26 | expSetupCmd.Flags().Bool("force", false, "Force setup even if already completed") 27 | } 28 | -------------------------------------------------------------------------------- /cmd/reload.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Manpreet Singh 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | 9 | "qswitch/utils" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // reloadCmd represents the reload command 15 | var reloadCmd = &cobra.Command{ 16 | Use: "reload", 17 | Short: "Reload keybinds for current flavour", 18 | Long: `Reload the keybinds configuration for the currently active flavour.`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | config := utils.LoadConfig() 21 | current := utils.ReadState() 22 | utils.ApplyKeybinds(current, config) 23 | fmt.Println("Config Reloaded") 24 | }, 25 | } 26 | 27 | func init() { 28 | rootCmd.AddCommand(reloadCmd) 29 | } 30 | -------------------------------------------------------------------------------- /quickshell/icons/mannu.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /cmake_uninstall.cmake.in: -------------------------------------------------------------------------------- 1 | if(NOT EXISTS "@CMAKE_BINARY_DIR@/install_manifest.txt") 2 | message(FATAL_ERROR "Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt") 3 | endif() 4 | 5 | file(READ "@CMAKE_BINARY_DIR@/install_manifest.txt" files) 6 | string(REGEX REPLACE "\n" ";" files "${files}") 7 | 8 | foreach(file ${files}) 9 | message(STATUS "Uninstalling $ENV{DESTDIR}${file}") 10 | if(IS_SYMLINK "$ENV{DESTDIR}${file}" OR EXISTS "$ENV{DESTDIR}${file}") 11 | exec_program( 12 | "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" 13 | OUTPUT_VARIABLE rm_out 14 | RETURN_VALUE rm_retval 15 | ) 16 | if(NOT "${rm_retval}" STREQUAL 0) 17 | message(FATAL_ERROR "Problem when removing $ENV{DESTDIR}${file}") 18 | endif() 19 | else() 20 | message(STATUS "File $ENV{DESTDIR}${file} does not exist.") 21 | endif() 22 | endforeach() 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 3 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= 6 | github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 7 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 8 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 9 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Example Configuration 2 | 3 | This directory contains an example configuration that I use in my personal setup. It demonstrates how to structure your `config.json` and keybind files. 4 | 5 | ## Structure 6 | 7 | - `config.json`: The main configuration file defining available flavours and their keybind mappings. 8 | - `keybinds/`: Directory containing flavour-specific keybind files. 9 | - `unbinds.conf`: Keybinds to unbind before applying new ones (if `unbinds: true` is set). 10 | - `caelestia.conf`: Keybinds for the "caelestia" flavour. 11 | - `noctalia.conf`: Keybinds for the "noctalia" flavour. 12 | 13 | ## Usage 14 | 15 | You can copy these files to your `~/.config/qswitch/` directory to get started. 16 | 17 | ```bash 18 | mkdir -p ~/.config/qswitch 19 | cp -r . ~/.config/qswitch/ 20 | ``` 21 | 22 | Make sure to adjust the `flavours` list in `config.json` to match the QuickShell themes you have installed. 23 | -------------------------------------------------------------------------------- /cmd/switchKeybinds.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Manpreet Singh 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | 9 | "qswitch/utils" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // switchKeybindsCmd represents the switchKeybinds command 15 | var switchKeybindsCmd = &cobra.Command{ 16 | Use: "switch-keybinds [flavour]", 17 | Short: "Switch only keybinds for a flavour", 18 | Long: `Apply keybinds for a specific flavour without switching the flavour itself.`, 19 | Args: cobra.ExactArgs(1), 20 | Run: func(cmd *cobra.Command, args []string) { 21 | config := utils.LoadConfig() 22 | flavour := args[0] 23 | if !utils.IsValidFlavour(flavour, config) { 24 | fmt.Println("Unknown flavour:", flavour) 25 | return 26 | } 27 | utils.ApplyKeybinds(flavour, config) 28 | fmt.Println("Switched keybinds to", flavour) 29 | }, 30 | } 31 | 32 | func init() { 33 | rootCmd.AddCommand(switchKeybindsCmd) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Manpreet Singh 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | 10 | "qswitch/utils" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // listCmd represents the list command 16 | var listCmd = &cobra.Command{ 17 | Use: "list", 18 | Short: "List available flavours", 19 | Long: `List all available flavours configured in the config.`, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | config := utils.LoadConfig() 22 | status, _ := cmd.Flags().GetBool("status") 23 | if status { 24 | type FlavourStatus struct { 25 | Name string `json:"name"` 26 | Installed bool `json:"installed"` 27 | } 28 | var statuses []FlavourStatus 29 | for _, f := range config.Flavours { 30 | statuses = append(statuses, FlavourStatus{ 31 | Name: f, 32 | Installed: utils.IsFlavourInstalled(f), 33 | }) 34 | } 35 | jsonData, _ := json.Marshal(statuses) 36 | fmt.Println(string(jsonData)) 37 | } else { 38 | for _, f := range config.Flavours { 39 | fmt.Println(f) 40 | } 41 | } 42 | }, 43 | } 44 | 45 | func init() { 46 | rootCmd.AddCommand(listCmd) 47 | listCmd.Flags().BoolP("status", "s", false, "Show installation status in JSON format") 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/update-completions.yml: -------------------------------------------------------------------------------- 1 | name: Update Completions 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | update-completions: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | token: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: "1.25" 23 | 24 | - name: Build qswitch 25 | run: go build -o qswitch . 26 | 27 | - name: Generate shell completions 28 | run: | 29 | ./qswitch completion --itrustmyself bash > completions/qswitch.bash 30 | ./qswitch completion --itrustmyself zsh > completions/qswitch.zsh 31 | ./qswitch completion --itrustmyself fish > completions/qswitch.fish 32 | 33 | - name: Commit and push changes 34 | run: | 35 | git config --global user.name 'github-actions[bot]' 36 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 37 | git add completions/ 38 | if git diff --staged --quiet; then 39 | echo "No changes to completions" 40 | else 41 | git commit -m "Update shell completions [automated]" 42 | git push 43 | fi 44 | -------------------------------------------------------------------------------- /AUR/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Manpreet Singh 2 | pkgname=qswitch-git 3 | pkgver=r43.2dd3549 4 | pkgrel=1 5 | pkgdesc="A utility for switching between QuickShell flavours in Hyprland" 6 | arch=('x86_64') 7 | url="https://github.com/MannuVilasara/qswitch" 8 | license=('GPL-3.0') 9 | depends=('glibc') 10 | makedepends=('go' 'git') 11 | optdepends=('quickshell: for the shell configurations') 12 | provides=('qswitch') 13 | conflicts=('qswitch') 14 | source=('git+https://github.com/MannuVilasara/qswitch.git') 15 | sha256sums=('SKIP') 16 | 17 | pkgver() { 18 | cd "qswitch" 19 | printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" 20 | } 21 | 22 | build() { 23 | cd "qswitch" 24 | go build -o qswitch . 25 | } 26 | 27 | package() { 28 | cd "qswitch" 29 | install -Dm755 qswitch "$pkgdir/usr/bin/qswitch" 30 | install -Dm644 man/qswitch.1 "$pkgdir/usr/share/man/man1/qswitch.1" 31 | install -Dm644 completions/qswitch.bash "$pkgdir/usr/share/bash-completion/completions/qswitch" 32 | install -Dm644 completions/qswitch.zsh "$pkgdir/usr/share/zsh/site-functions/_qswitch" 33 | install -Dm644 completions/qswitch.fish "$pkgdir/usr/share/fish/vendor_completions.d/qswitch.fish" 34 | install -Dm644 example/config.json "$pkgdir/usr/share/doc/qswitch/config.json" 35 | install -Dm644 README.md "$pkgdir/usr/share/doc/qswitch/README.md" 36 | } 37 | -------------------------------------------------------------------------------- /utils/state.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | var stateFile = os.Getenv("HOME") + "/.switch_state" 11 | var panelPidFile = os.Getenv("HOME") + "/.qswitch_panel_pid" 12 | 13 | func ReadState() string { 14 | data, err := os.ReadFile(stateFile) 15 | if err != nil { 16 | return "" 17 | } 18 | return strings.TrimSpace(string(data)) 19 | } 20 | 21 | func WriteState(f string) { os.WriteFile(stateFile, []byte(f), 0644) } 22 | 23 | // IsFlavourInstalled checks if a flavour configuration exists 24 | // A flavour is installed if it has a directory in /etc/xdg/quickshell/ 25 | // or ~/.config/quickshell/, and the required command is available 26 | func IsFlavourInstalled(flavour string) bool { 27 | 28 | if flavour == "dms" { 29 | // Check if dms command is available 30 | _, err := exec.LookPath("dms") 31 | if err != nil { 32 | return false 33 | } 34 | return true 35 | } 36 | 37 | // Check in ~/.config/quickshell/ 38 | userPath := filepath.Join(os.Getenv("HOME"), ".config", "quickshell", flavour) 39 | if _, err := os.Stat(userPath); err == nil { 40 | return true 41 | } 42 | 43 | // Check in /etc/xdg/quickshell/ 44 | systemPath := filepath.Join("/etc/xdg/quickshell", flavour) 45 | if _, err := os.Stat(systemPath); err == nil { 46 | return true 47 | } 48 | 49 | return false 50 | } 51 | 52 | // CheckFirstRun checks if state file exists 53 | // Returns true if setup is needed (never run before) 54 | func CheckFirstRun() bool { 55 | // Skip check if user has already run qswitch before (state file exists) 56 | if _, err := os.Stat(stateFile); err == nil { 57 | return false 58 | } 59 | return true 60 | } 61 | -------------------------------------------------------------------------------- /utils/config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "path/filepath" 7 | "slices" 8 | ) 9 | 10 | type Config struct { 11 | Flavours []string `json:"flavours"` 12 | Keybinds map[string]string `json:"keybinds"` 13 | Unbinds bool `json:"unbinds"` 14 | PanelKeybind string `json:"panel_keybind"` 15 | } 16 | 17 | var defaultConfig = Config{ 18 | Flavours: []string{}, 19 | Keybinds: map[string]string{}, 20 | Unbinds: false, 21 | PanelKeybind: "Super+Alt, P", 22 | } 23 | 24 | func LoadConfig() Config { 25 | configDir := filepath.Join(os.Getenv("HOME"), ".config", "qswitch") 26 | configPath := filepath.Join(configDir, "config.json") 27 | 28 | // Create dir if not exists 29 | os.MkdirAll(configDir, 0755) 30 | 31 | // Create keybinds dir 32 | keybindsDir := filepath.Join(configDir, "keybinds") 33 | os.MkdirAll(keybindsDir, 0755) 34 | 35 | // Read file 36 | data, err := os.ReadFile(configPath) 37 | if err != nil { 38 | // Create with default 39 | defaultData, _ := json.MarshalIndent(defaultConfig, "", " ") 40 | os.WriteFile(configPath, defaultData, 0644) 41 | return defaultConfig 42 | } 43 | 44 | var config Config 45 | json.Unmarshal(data, &config) 46 | if config.Keybinds == nil { 47 | config.Keybinds = defaultConfig.Keybinds 48 | // Write back updated config 49 | updatedData, _ := json.MarshalIndent(config, "", " ") 50 | os.WriteFile(configPath, updatedData, 0644) 51 | } 52 | 53 | if config.PanelKeybind == "" { 54 | config.PanelKeybind = defaultConfig.PanelKeybind 55 | // Write back updated config 56 | updatedData, _ := json.MarshalIndent(config, "", " ") 57 | os.WriteFile(configPath, updatedData, 0644) 58 | } 59 | return config 60 | } 61 | 62 | func IsValidFlavour(name string, config Config) bool { 63 | return slices.Contains(config.Flavours, name) 64 | } 65 | -------------------------------------------------------------------------------- /example/keybinds/caelestia.conf: -------------------------------------------------------------------------------- 1 | # caelestia keybindings configuration file 2 | 3 | 4 | bind = Super, Tab, global, caelestia:launcher 5 | bindin = Super, catchall, global, caelestia:launcherInterrupt 6 | bindin = Super, mouse:272, global, caelestia:launcherInterrupt 7 | bindin = Super, mouse:273, global, caelestia:launcherInterrupt 8 | bindin = Super, mouse:274, global, caelestia:launcherInterrupt 9 | bindin = Super, mouse:275, global, caelestia:launcherInterrupt 10 | bindin = Super, mouse:276, global, caelestia:launcherInterrupt 11 | bindin = Super, mouse:277, global, caelestia:launcherInterrupt 12 | bindin = Super, mouse_up, global, caelestia:launcherInterrupt 13 | bindin = Super, mouse_down, global, caelestia:launcherInterrupt 14 | bindl = Ctrl+Super, Space, global, caelestia:mediaToggle 15 | bindl = , XF86AudioPlay, global, caelestia:mediaToggle 16 | bindl = , XF86AudioPause, global, caelestia:mediaToggle 17 | bindl = Ctrl+Super, Equal, global, caelestia:mediaNext 18 | bindl = , XF86AudioNext, global, caelestia:mediaNext 19 | bindl = Ctrl+Super, Minus, global, caelestia:mediaPrev 20 | bindl = , XF86AudioPrev, global, caelestia:mediaPrev 21 | bindl = , XF86AudioStop, global, caelestia:mediaStop 22 | bind = Super+Ctrl+Alt,U, exec, ~/.local/bin/switch.sh 23 | bind = Super, V, exec, pkill fuzzel || caelestia clipboard 24 | bind = Super+Alt, V, exec, pkill fuzzel || caelestia clipboard -d 25 | bind = Super, Period, exec, pkill fuzzel || caelestia emoji -p 26 | # bindl = Ctrl+Shift+Alt, V, exec, sleep 0.5s && ydotool type -d 1 "$(cliphist list | head -1 | cliphist decode)" # Alternate paste 27 | bind = Super, A, global, caelestia:showall 28 | # unbind= Super, C 29 | bind = Super, C, global, caelestia:clearNotifs 30 | # Testing 31 | bindl = Super+Alt, f12, exec, notify-send -u low -i dialog-information-symbolic 'Test notification' "Here's a really long message to test truncation and wrapping\nYou can middle click or flick this notification to dismiss it!" -a 'Shell' -A "Test1=I got it!" -A "Test2=Another action" 32 | 33 | -------------------------------------------------------------------------------- /cmd/apply.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Manpreet Singh 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | 9 | "qswitch/utils" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // applyCmd represents the apply command 15 | var applyCmd = &cobra.Command{ 16 | Use: "apply [flavour]", 17 | Short: "Switch to or apply a flavour", 18 | Long: `Switch to a specific flavour or apply the current flavour's configuration.`, 19 | Run: func(cmd *cobra.Command, args []string) { 20 | config := utils.LoadConfig() 21 | currentFlag, _ := cmd.Flags().GetBool("current") 22 | if currentFlag { 23 | currentFlavour := utils.ReadState() 24 | if !utils.IsValidFlavour(currentFlavour, config) { 25 | fmt.Println("No valid current flavour set.") 26 | return 27 | } 28 | if currentFlavour == "" { 29 | fmt.Println("No flavour is currently set.") 30 | return 31 | } 32 | utils.ApplyFlavour(currentFlavour, config) 33 | fmt.Println("Applied current flavour:", currentFlavour) 34 | return 35 | } 36 | if len(args) != 1 { 37 | fmt.Println( 38 | "Invalid usage. Use 'qswitch apply ' or 'qswitch apply --current'.", 39 | ) 40 | return 41 | } 42 | flavour := args[0] 43 | if !utils.IsValidFlavour(flavour, config) { 44 | fmt.Println("Unknown flavour:", flavour) 45 | fmt.Println("Run 'qswitch --help' to list flavours.") 46 | return 47 | } 48 | // Check if the flavour is installed 49 | if !utils.IsFlavourInstalled(flavour) { 50 | fmt.Println("Flavour not installed:", flavour) 51 | fmt.Println("Install it to /etc/xdg/quickshell/" + flavour + " first.") 52 | return 53 | } 54 | // Check if the flavour is already running 55 | current := utils.ReadState() 56 | if current == flavour { 57 | fmt.Println("Already running:", flavour) 58 | return 59 | } 60 | utils.WriteState(flavour) 61 | utils.ApplyFlavour(flavour, config) 62 | fmt.Println("Switched to", flavour) 63 | }, 64 | } 65 | 66 | func init() { 67 | rootCmd.AddCommand(applyCmd) 68 | applyCmd.Flags().Bool("current", false, "Apply the current flavour") 69 | } 70 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Manpreet Singh 3 | */ 4 | package cmd 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "qswitch/utils" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // rootCmd represents the base command when called without any subcommands 16 | var rootCmd = &cobra.Command{ 17 | Use: "qswitch", 18 | Short: "Switch between Quickshell flavours", 19 | Long: `qswitch is a CLI tool to switch between different Quickshell configurations (flavours). 20 | 21 | Usage: 22 | qswitch Cycle to the next flavour 23 | qswitch apply Switch to a specific flavour 24 | qswitch [command] Run a specific command`, 25 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 26 | bypass, _ := cmd.Flags().GetBool("itrustmyself") 27 | if !bypass { 28 | // Allow certain commands without setup 29 | switch cmd.Name() { 30 | case "help", "list", "current", "exp-setup": 31 | return 32 | } 33 | if utils.CheckFirstRun() { 34 | utils.ShowSetupMessage() 35 | os.Exit(0) 36 | } 37 | } 38 | }, 39 | Run: func(cmd *cobra.Command, args []string) { 40 | if len(args) == 0 { 41 | config := utils.LoadConfig() 42 | utils.Cycle(config) 43 | } else { 44 | fmt.Println("Invalid usage. Use 'qswitch' to cycle or 'qswitch apply ' to switch.") 45 | } 46 | }, 47 | } 48 | 49 | // Execute adds all child commands to the root command and sets flags appropriately. 50 | // This is called by main.main(). It only needs to happen once to the rootCmd. 51 | func Execute() { 52 | err := rootCmd.Execute() 53 | if err != nil { 54 | os.Exit(1) 55 | } 56 | } 57 | 58 | func init() { 59 | // Here you will define your flags and configuration settings. 60 | // Cobra supports persistent flags, which, if defined here, 61 | // will be global for your application. 62 | 63 | rootCmd.PersistentFlags().Bool("itrustmyself", false, "Bypass setup check") 64 | 65 | // Cobra also supports local flags, which will only run 66 | // when this action is called directly. 67 | rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 68 | } 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to qswitch 2 | 3 | Thank you for your interest in contributing to qswitch! I am open to contributions, whether it's bug fixes, new features, or documentation improvements. 4 | 5 | ## Code Structure 6 | 7 | The project is written in Go and uses CMake for building. 8 | 9 | - **`cmd/qswitch/`**: Contains the source code for the main application. 10 | - **`main.go`**: The entry point. Handles CLI argument parsing and high-level command logic. 11 | - **`config.go`**: Handles loading and parsing the `config.json` file. 12 | - **`state.go`**: Manages the application state (current flavour) and checks for installed flavours. 13 | - **`actions.go`**: Contains the core logic for performing actions like switching flavours, applying keybinds, and running the setup. 14 | - **`quickshell/`**: Contains the QML files for the QuickSwitch panel. 15 | - **`completions/`**: Shell completion scripts. 16 | 17 | ## How it Works 18 | 19 | `qswitch` works by managing the `qs` (QuickShell) process and Hyprland keybinds dynamically. 20 | 21 | 1. **Configuration**: It reads `~/.config/qswitch/config.json` to know about available flavours and their specific keybind mappings. 22 | 2. **State**: It tracks the currently active flavour in `~/.switch_state`. 23 | 3. **Switching**: When switching flavours: 24 | - It kills the current `qs` process. 25 | - It starts a new `qs` instance with the selected configuration (`qs -c `). 26 | - It generates `~/.config/qswitch/qswitch.conf`. This file contains `source` directives to load flavour-specific keybinds into Hyprland. 27 | - Hyprland automatically picks up these changes because `hyprland.conf` sources `qswitch.conf`. 28 | 29 | ## Development 30 | 31 | 1. Clone the repository. 32 | 2. Make your changes in the `cmd/qswitch` directory. 33 | 3. Build the project using CMake: 34 | 35 | ```bash 36 | mkdir build 37 | cd build 38 | cmake .. 39 | make 40 | ``` 41 | 42 | 4. Test your changes with the built binary. 43 | 44 | ## Pull Requests 45 | 46 | Please ensure your code is formatted (standard Go formatting) and clearly documented. Feel free to open a PR! 47 | -------------------------------------------------------------------------------- /man/qswitch.1: -------------------------------------------------------------------------------- 1 | 2 | .TH QSWITCH 1 "December 2025" "qswitch 1.0" "User Commands" 3 | .SH NAME 4 | qswitch \- switch between QS flavours 5 | 6 | .SH SYNOPSIS 7 | .B qswitch 8 | [\fIflavour\fR] 9 | .br 10 | .B qswitch 11 | [\fB--help\fR] 12 | .br 13 | .B qswitch 14 | [\fB--list\fR] 15 | .br 16 | .B qswitch 17 | [\fB--current\fR] 18 | .br 19 | .B qswitch 20 | [\fB--panel\fR] 21 | .br 22 | .B qswitch 23 | \fBapply --current\fR 24 | 25 | .SH DESCRIPTION 26 | \fBqswitch\fR is a small utility for Hyprland users that switches between 27 | different QS flavours. A flavour is a configuration profile used by the 28 | \fBqs\fR compositor helper. 29 | 30 | When run without arguments, \fBqswitch\fR cycles to the next flavour 31 | in its internal list. 32 | 33 | .SH CONFIGURATION 34 | \fBswitch\fR reads its configuration from 35 | .I ~/.config/qswitch/config.json 36 | .br 37 | The configuration includes a list of flavours and their associated keybind files. 38 | .br 39 | Keybind files are stored in 40 | .I ~/.config/qswitch/keybinds/ 41 | and are sourced into Hyprland's configuration when switching flavours. 42 | 43 | .SH FLAVOURS 44 | The available flavours are configurable via the configuration file 45 | .I ~/.config/qswitch/config.json 46 | .br 47 | By default, the following flavours are available: 48 | 49 | .IP \[bu] 2 50 | ii 51 | .IP \[bu] 2 52 | caelestia 53 | .IP \[bu] 2 54 | noctalia-shell 55 | 56 | .SH OPTIONS 57 | .TP 58 | .BR \-\-help ", " -h 59 | Show this help page. 60 | .TP 61 | .BR \-\-list 62 | List available flavours. 63 | .TP 64 | .BR \-\-current 65 | Show the currently active flavour. 66 | .TP 67 | .BR \-\-panel 68 | Toggle the QuickSwitch panel. 69 | .TP 70 | .B apply --current 71 | Re-apply the configuration for the current flavour. 72 | 73 | .SH FILES 74 | .TP 75 | .I ~/.switch_state 76 | Stores the last active flavour for cycling. 77 | .TP 78 | .I ~/.config/qswitch/config.json 79 | Configuration file containing available flavours and keybind settings. 80 | .TP 81 | .I ~/.config/qswitch/keybinds/ 82 | Directory containing flavour-specific keybind files. 83 | .TP 84 | .I ~/.config/qswitch/qswitch.conf 85 | Generated file sourcing the active flavour's keybinds. 86 | 87 | .SH EXAMPLES 88 | .TP 89 | Switch directly to a flavour: 90 | .br 91 | switch ii 92 | .TP 93 | Cycle to the next flavour: 94 | .br 95 | switch 96 | 97 | .SH AUTHOR 98 | Written by Manpreet Singh 99 | 100 | .SH SEE ALSO 101 | qs(1), hyprctl(1) 102 | -------------------------------------------------------------------------------- /quickshell/icons/arch.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 34 | 37 | 41 | 45 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(qswitch-go NONE) 3 | 4 | # Path to Go executable 5 | set(GO_EXECUTABLE "go") 6 | 7 | # Output directory for build products 8 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 9 | 10 | # ------------------------------- 11 | # Build Go binary 12 | # ------------------------------- 13 | add_custom_command( 14 | OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/qswitch 15 | COMMAND ${GO_EXECUTABLE} build -o ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/qswitch . 16 | WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} 17 | DEPENDS 18 | ${CMAKE_SOURCE_DIR}/main.go 19 | ${CMAKE_SOURCE_DIR}/cmd/root.go 20 | ${CMAKE_SOURCE_DIR}/cmd/list.go 21 | ${CMAKE_SOURCE_DIR}/cmd/current.go 22 | ${CMAKE_SOURCE_DIR}/cmd/panel.go 23 | ${CMAKE_SOURCE_DIR}/cmd/reload.go 24 | ${CMAKE_SOURCE_DIR}/cmd/switchKeybinds.go 25 | ${CMAKE_SOURCE_DIR}/cmd/apply.go 26 | ${CMAKE_SOURCE_DIR}/cmd/expSetup.go 27 | ${CMAKE_SOURCE_DIR}/cmd/autofix.go 28 | ${CMAKE_SOURCE_DIR}/utils/config.go 29 | ${CMAKE_SOURCE_DIR}/utils/state.go 30 | ${CMAKE_SOURCE_DIR}/utils/actions.go 31 | ${CMAKE_SOURCE_DIR}/utils/help.go 32 | ${CMAKE_SOURCE_DIR}/utils/autofix.go 33 | COMMENT "Building qswitch Go binary" 34 | ) 35 | 36 | add_custom_target(qswitch ALL 37 | DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/qswitch 38 | ) 39 | 40 | # ------------------------------- 41 | # Install binary with executable permissions 42 | # ------------------------------- 43 | install( 44 | FILES ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/qswitch 45 | DESTINATION bin 46 | PERMISSIONS 47 | OWNER_READ OWNER_WRITE OWNER_EXECUTE 48 | GROUP_READ GROUP_EXECUTE 49 | WORLD_READ WORLD_EXECUTE 50 | ) 51 | 52 | # ------------------------------- 53 | # Install manpage 54 | # ------------------------------- 55 | install( 56 | FILES ${CMAKE_SOURCE_DIR}/man/qswitch.1 57 | DESTINATION share/man/man1 58 | ) 59 | 60 | # ------------------------------- 61 | # Shell completions 62 | # ------------------------------- 63 | # Bash 64 | install( 65 | FILES ${CMAKE_SOURCE_DIR}/completions/qswitch.bash 66 | DESTINATION share/bash-completion/completions 67 | RENAME qswitch 68 | ) 69 | 70 | # Zsh 71 | install( 72 | FILES ${CMAKE_SOURCE_DIR}/completions/qswitch.zsh 73 | DESTINATION share/zsh/site-functions 74 | RENAME _qswitch 75 | ) 76 | 77 | # Fish 78 | install( 79 | FILES ${CMAKE_SOURCE_DIR}/completions/qswitch.fish 80 | DESTINATION share/fish/vendor_completions.d 81 | ) 82 | # ------------------------------- 83 | # QuickSwitchPanel QML 84 | # ------------------------------- 85 | install( 86 | FILES ${CMAKE_SOURCE_DIR}/quickshell/shell.qml 87 | DESTINATION /etc/xdg/quickshell/qswitch/ 88 | PERMISSIONS 89 | OWNER_READ OWNER_WRITE 90 | GROUP_READ 91 | WORLD_READ 92 | ) 93 | 94 | # ------------------------------- 95 | # Flavour icons 96 | # ------------------------------- 97 | install( 98 | DIRECTORY ${CMAKE_SOURCE_DIR}/quickshell/icons/ 99 | DESTINATION /etc/xdg/quickshell/qswitch/icons/ 100 | FILES_MATCHING 101 | PATTERN "*.svg" 102 | PATTERN "*.png" 103 | PERMISSIONS 104 | OWNER_READ OWNER_WRITE 105 | GROUP_READ 106 | WORLD_READ 107 | ) 108 | 109 | install(CODE "execute_process(COMMAND chown -R root:root /etc/xdg/quickshell/qswitch/)") 110 | 111 | # ------------------------------- 112 | # Uninstall Target 113 | # ------------------------------- 114 | if(NOT TARGET uninstall) 115 | configure_file( 116 | "${CMAKE_CURRENT_SOURCE_DIR}/cmake_uninstall.cmake.in" 117 | "${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake" 118 | IMMEDIATE @ONLY) 119 | 120 | add_custom_target(uninstall 121 | COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake) 122 | endif() 123 | -------------------------------------------------------------------------------- /utils/autofix.go: -------------------------------------------------------------------------------- 1 | // this file checks and applies autofix for known issues 2 | package utils 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | var qswitchCacheDir = filepath.Join(os.Getenv("HOME"), ".cache", "qswitch") 12 | var hyprlandFile = filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf") 13 | var qswitchDir = filepath.Join(os.Getenv("HOME"), ".config", "qswitch") 14 | var sourceLine = "source=" + qswitchCacheDir + "/qswitch.conf" 15 | var wrongSourceLines = []string{ 16 | "source=" + qswitchDir + "/qswitch.conf", 17 | "source=" + "~/.config/qswitch/qswitch.conf", 18 | "source=" + "~/.cache/qswitch/qswitch.conf", 19 | } 20 | 21 | func dirExists(path string) bool { 22 | _, err := os.Stat(path) 23 | return err == nil 24 | } 25 | 26 | // ApplyAutofix checks for known issues and applies fixes 27 | func ApplyAutofix() { 28 | fmt.Println("Checking for autofixes...") 29 | _, err := os.Stat(hyprlandFile) 30 | hyprlandExists := err == nil 31 | qswitchCacheExists := dirExists(qswitchCacheDir) 32 | qswitchConfigExists := dirExists(qswitchDir) 33 | 34 | // Check if Hyprland config file exists 35 | if !hyprlandExists { 36 | fmt.Println( 37 | "Hyprland configuration file not found. Please ensure Hyprland is installed and configured correctly.", 38 | ) 39 | return 40 | } else { 41 | fmt.Println("Hyprland configuration file found.") 42 | } 43 | 44 | // Check and create QSwitch cache directory if it doesn't exist 45 | if !qswitchCacheExists { 46 | fmt.Println("QSwitch cache directory not found. Creating it now...") 47 | err := os.MkdirAll(qswitchCacheDir, 0755) 48 | if err != nil { 49 | fmt.Printf("Failed to create QSwitch cache directory: %v\n", err) 50 | return 51 | } else { 52 | fmt.Println("QSwitch cache directory created successfully.") 53 | } 54 | } else { 55 | fmt.Println("QSwitch cache directory exists.") 56 | } 57 | 58 | if !qswitchConfigExists { 59 | fmt.Println("QSwitch configuration directory not found. Creating it now...") 60 | err := os.MkdirAll(qswitchDir, 0755) 61 | if err != nil { 62 | fmt.Printf("Failed to create QSwitch configuration directory: %v\n", err) 63 | } else { 64 | fmt.Println("QSwitch configuration directory created successfully.") 65 | } 66 | } else { 67 | fmt.Println("QSwitch configuration directory exists.") 68 | } 69 | 70 | hyprcontent, err := os.ReadFile(hyprlandFile) 71 | if err != nil { 72 | fmt.Printf("Error reading Hyprland configuration file: %v\n", err) 73 | return 74 | } 75 | 76 | if !strings.Contains(string(hyprcontent), sourceLine) { 77 | fmt.Println("QSwitch configuration not found in Hyprland config. Adding it now...") 78 | f, err := os.OpenFile(hyprlandFile, os.O_APPEND|os.O_WRONLY, 0644) 79 | if err != nil { 80 | fmt.Printf("Error opening Hyprland configuration file for appending: %v\n", err) 81 | return 82 | } 83 | defer f.Close() 84 | 85 | _, err = f.WriteString("\n" + sourceLine + "\n") 86 | if err != nil { 87 | fmt.Printf("Error writing to Hyprland configuration file: %v\n", err) 88 | return 89 | } else { 90 | fmt.Println("QSwitch configuration added to Hyprland config successfully.") 91 | } 92 | } else { 93 | fmt.Println("QSwitch configuration already present in Hyprland config.") 94 | } 95 | 96 | updatedContent := string(hyprcontent) 97 | for _, wrongLine := range wrongSourceLines { 98 | if strings.Contains(updatedContent, wrongLine) { 99 | fmt.Printf( 100 | "Incorrect QSwitch source line found in Hyprland config: %s. Removing it now...\n", 101 | wrongLine, 102 | ) 103 | updatedContent = strings.ReplaceAll(updatedContent, wrongLine+"\n", "") 104 | updatedContent = strings.ReplaceAll(updatedContent, wrongLine, "") 105 | } 106 | } 107 | if updatedContent != string(hyprcontent) { 108 | err = os.WriteFile(hyprlandFile, []byte(updatedContent), 0644) 109 | if err != nil { 110 | fmt.Printf("Error updating Hyprland configuration file: %v\n", err) 111 | return 112 | } else { 113 | fmt.Println("Incorrect QSwitch source lines removed from Hyprland config successfully.") 114 | } 115 | } else { 116 | fmt.Println("No incorrect QSwitch source lines found in Hyprland config.") 117 | } 118 | 119 | fmt.Println("Autofix process completed.") 120 | } 121 | -------------------------------------------------------------------------------- /quickshell/icons/ii.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 33 | 36 | 42 | 49 | 56 | 64 | 69 | 75 | 80 | 81 | 83 | 90 | 93 | 97 | 98 | 105 | 108 | 112 | 113 | 115 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /utils/actions.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | func ApplyKeybinds(flavour string, config Config) { 12 | // Handle keybinds 13 | qswitchDir := filepath.Join(os.Getenv("HOME"), ".config", "qswitch") 14 | qswitchCacheDir := filepath.Join(os.Getenv("HOME"), ".cache", "qswitch") 15 | os.MkdirAll(qswitchDir, 0755) 16 | if err := os.MkdirAll(qswitchCacheDir, 0755); err != nil { 17 | fmt.Printf("Error creating cache directory: %v\n", err) 18 | return 19 | } 20 | keybindsFile := filepath.Join(qswitchCacheDir, "qswitch.conf") 21 | 22 | var contentParts []string 23 | 24 | // Check for unbinds if enabled 25 | if config.Unbinds && config.Keybinds[flavour] != "default" { 26 | unbindsPath := filepath.Join( 27 | os.Getenv("HOME"), 28 | ".config", 29 | "qswitch", 30 | "keybinds", 31 | "unbinds.conf", 32 | ) 33 | if _, err := os.Stat(unbindsPath); err == nil { 34 | contentParts = append(contentParts, "source="+unbindsPath) 35 | } else { 36 | fmt.Printf("Warning: unbinds.conf not found at %s\n", unbindsPath) 37 | } 38 | } 39 | 40 | // Add flavour keybinds 41 | if config.Keybinds[flavour] == "default" { 42 | contentParts = append(contentParts, "# Default") 43 | } else { 44 | keybindPath := filepath.Join(os.Getenv("HOME"), ".config", "qswitch", "keybinds", config.Keybinds[flavour]) 45 | if _, err := os.Stat(keybindPath); err == nil { 46 | contentParts = append(contentParts, "source="+keybindPath) 47 | } else { 48 | fmt.Printf("Warning: keybind file %s not found for flavour %s\n", config.Keybinds[flavour], flavour) 49 | } 50 | } 51 | 52 | // Add QuickSwitchPanel keybind 53 | contentParts = append(contentParts, "bind="+config.PanelKeybind+", exec, qswitch panel") 54 | 55 | content := strings.Join(contentParts, "\n") 56 | if err := os.WriteFile(keybindsFile, []byte(content), 0644); err != nil { 57 | fmt.Printf("Error writing keybinds file: %v\n", err) 58 | } 59 | } 60 | 61 | func ApplyFlavour(flavour string, config Config) { 62 | // kill old qs 63 | exec.Command("pkill", "-x", "qs").Run() 64 | exec.Command("caelestia", "shell", "-k").Run() 65 | 66 | // start new one 67 | if flavour == "dms" { 68 | exec.Command("dms", "run", "-d").Run() 69 | } else { 70 | exec.Command("hyprctl", "dispatch", "exec", "qs -c "+flavour).Run() 71 | } 72 | 73 | ApplyKeybinds(flavour, config) 74 | } 75 | 76 | // TogglePanel opens the panel if not running, closes it if running 77 | func TogglePanel() { 78 | // Check if panel is already running by reading PID file 79 | pidData, err := os.ReadFile(panelPidFile) 80 | if err == nil { 81 | pid := strings.TrimSpace(string(pidData)) 82 | // Check if process is still running 83 | checkCmd := exec.Command("kill", "-0", pid) 84 | if checkCmd.Run() == nil { 85 | // Process is running, kill it 86 | exec.Command("kill", pid).Run() 87 | os.Remove(panelPidFile) 88 | return 89 | } 90 | } 91 | 92 | // Panel not running, start it 93 | cmd := exec.Command("qs", "-c", "qswitch") 94 | cmd.Start() 95 | if cmd.Process != nil { 96 | os.WriteFile(panelPidFile, []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644) 97 | } 98 | } 99 | 100 | func Cycle(config Config) { 101 | current := ReadState() 102 | 103 | // Find the first installed flavour for fallback 104 | firstInstalled := "" 105 | for _, f := range config.Flavours { 106 | if IsFlavourInstalled(f) { 107 | firstInstalled = f 108 | break 109 | } 110 | } 111 | 112 | if firstInstalled == "" { 113 | fmt.Println("No installed flavours found.") 114 | return 115 | } 116 | 117 | if current == "" { 118 | WriteState(firstInstalled) 119 | ApplyFlavour(firstInstalled, config) 120 | fmt.Println("Switched to", firstInstalled) 121 | return 122 | } 123 | 124 | // Find current index and cycle to next installed flavour 125 | currentIdx := -1 126 | for i, f := range config.Flavours { 127 | if f == current { 128 | currentIdx = i 129 | break 130 | } 131 | } 132 | 133 | if currentIdx == -1 { 134 | // Current not found, use first installed 135 | WriteState(firstInstalled) 136 | ApplyFlavour(firstInstalled, config) 137 | fmt.Println("Switched to", firstInstalled) 138 | return 139 | } 140 | 141 | // Find next installed flavour 142 | for i := 1; i <= len(config.Flavours); i++ { 143 | nextIdx := (currentIdx + i) % len(config.Flavours) 144 | next := config.Flavours[nextIdx] 145 | if IsFlavourInstalled(next) { 146 | WriteState(next) 147 | ApplyFlavour(next, config) 148 | fmt.Println("Switched to", next) 149 | return 150 | } 151 | } 152 | 153 | fmt.Println("No other installed flavours to switch to.") 154 | } 155 | 156 | func Setup(config Config, force bool) { 157 | // Check if state file exists 158 | if _, err := os.Stat(stateFile); err == nil && !force { 159 | fmt.Println("Setup already completed (state file exists).") 160 | return 161 | } 162 | 163 | // Create state file if it doesn't exist 164 | if err := os.WriteFile(stateFile, []byte(""), 0644); err != nil { 165 | fmt.Printf("Error creating state file: %v\n", err) 166 | } 167 | 168 | // Create cache directory 169 | qswitchCacheDir := filepath.Join(os.Getenv("HOME"), ".cache", "qswitch") 170 | if err := os.MkdirAll(qswitchCacheDir, 0755); err != nil { 171 | fmt.Printf("Error creating cache directory: %v\n", err) 172 | return 173 | } 174 | 175 | keybindsFile := filepath.Join(qswitchCacheDir, "qswitch.conf") 176 | content := "bind=" + config.PanelKeybind + ", exec, qswitch panel" 177 | if err := os.WriteFile(keybindsFile, []byte(content), 0644); err != nil { 178 | fmt.Printf("Error creating keybinds file: %v\n", err) 179 | return 180 | } 181 | hyprlandFile := filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf") 182 | 183 | // Check if already sourced 184 | hyprContent, err := os.ReadFile(hyprlandFile) 185 | if err == nil { 186 | sourceLine := "source=" + qswitchCacheDir + "/qswitch.conf" 187 | if strings.Contains(string(hyprContent), sourceLine) { 188 | fmt.Println("Setup completed (already sourced)") 189 | return 190 | } 191 | } 192 | 193 | f, err := os.OpenFile(hyprlandFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 194 | if err != nil { 195 | fmt.Println("Error opening hyprland.conf:", err) 196 | return 197 | } 198 | defer f.Close() 199 | f.WriteString("\nsource=" + qswitchCacheDir + "/qswitch.conf\n") 200 | fmt.Println("Setup completed") 201 | } 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qswitch 2 | 3 | A utility for switching between QuickShell flavours in Hyprland. 4 | 5 | ## Support 6 | 7 | **First of all, DM me on Discord `@dev_mannu` if you want to use this and don't understand how to make it work.** 8 | 9 | > NOTE: So far I only suggest using them with ii, caelestia & noctalia. it might not work with all. 10 | > if you're using it after 13th of December 2025, dms is now supported as well. but it might not be stable. 11 | > This code written here is fucking weird and I won't even suggest anyone writing it like I did. but if it works, it works. 12 | > I'll refactor it after my exams probably 13 | 14 | ## Description 15 | 16 | `qswitch` allows you to switch between different QuickShell configurations (flavours) seamlessly. It manages the QS process, updates Hyprland keybinds, and sources flavour-specific keybind files. 17 | 18 | It is designed to be flexible and work with any QuickShell configuration installed in standard locations. 19 | **Important:** Ensure your shells are installed at `/etc/xdg/quickshell` or `~/.config/quickshell`. (except dms) 20 | 21 | ## Installation 22 | 23 | ### Prerequisites 24 | 25 | - Go 1.25.4 or later 26 | - CMake 3.15 or later (optional, for CMake build) 27 | - Hyprland and QuickShell installed 28 | 29 | ### Configuration Files 30 | 31 | You can find an example configuration in the `example/` directory of this repository. 32 | Also, you can check my personal configuration here: 33 | [https://github.com/MannuVilasara/commaflies/tree/main/qswitch/.config/qswitch](https://github.com/MannuVilasara/commaflies/tree/main/qswitch/.config/qswitch) 34 | 35 | ### Build and Install 36 | 37 | #### Option 1: CMake Build (Recommended) 38 | 39 | 1. Clone the repository: 40 | 41 | ```bash 42 | git clone https://github.com/MannuVilasara/qswitch.git 43 | cd qswitch 44 | ``` 45 | 46 | 2. Build the project: 47 | 48 | ```bash 49 | mkdir build 50 | cd build 51 | cmake .. 52 | make 53 | ``` 54 | 55 | 3. Install system-wide (requires root): 56 | 57 | ```bash 58 | sudo make install 59 | ``` 60 | 61 | This installs: 62 | 63 | - The `qswitch` binary to `/usr/local/bin` 64 | - Man page to `/usr/local/share/man/man1` 65 | - Shell completions to appropriate directories 66 | - shell.qml to `/etc/xdg/quickshell/qswitch/` 67 | 68 | #### Option 2: Go Build 69 | 70 | 1. Clone the repository: 71 | 72 | ```bash 73 | git clone https://github.com/MannuVilasara/qswitch.git 74 | cd qswitch 75 | ``` 76 | 77 | 2. Build and install: 78 | 79 | ```bash 80 | go build -o qswitch . 81 | sudo cp qswitch /usr/local/bin/ 82 | sudo cp man/qswitch.1 /usr/local/share/man/man1/ 83 | sudo cp completions/qswitch.bash /usr/share/bash-completion/completions/qswitch 84 | sudo cp completions/qswitch.zsh /usr/share/zsh/site-functions/_qswitch 85 | sudo cp completions/qswitch.fish /usr/share/fish/vendor_completions.d/qswitch.fish 86 | sudo mkdir -p /etc/xdg/quickshell/qswitch 87 | sudo cp -r quickshell/* /etc/xdg/quickshell/qswitch 88 | ``` 89 | 90 | ### Uninstall (CMake) 91 | 92 | To uninstall the project: 93 | 94 | ```bash 95 | cd build 96 | sudo make uninstall 97 | ``` 98 | 99 | ## Configuration 100 | 101 | Configuration is stored in `~/.config/qswitch/config.json`: 102 | 103 | ```json 104 | { 105 | "flavours": ["ii", "caelestia", "noctalia-shell"], 106 | "unbinds": true, 107 | "keybinds": { 108 | "ii": "default", 109 | "caelestia": "caelestia.conf", 110 | "noctalia-shell": "noctalia.conf" 111 | }, 112 | "panel_keybind": "Super+Alt, P" 113 | } 114 | ``` 115 | 116 | - **flavours**: List of available flavours. 117 | - **unbinds**: (Optional) Boolean. If true, sources `~/.config/qswitch/keybinds/unbinds.conf` before applying flavour-specific keybinds (except for "default" flavour). Useful for unbinding keys that might conflict. 118 | - **keybinds**: Maps each flavour to a keybind file in `~/.config/qswitch/keybinds/`. Use "default" for the base configuration. 119 | - **panel_keybind**: (Optional) The keybind to open the QuickSwitch panel. Defaults to "Super+Alt, P". 120 | 121 | Keybind files (e.g., `caelestia.conf`) contain Hyprland keybind definitions. 122 | 123 | The tool generates `~/.cache/qswitch/qswitch.conf` with the appropriate source and bind commands, and sources it in `~/.config/hypr/hyprland.conf`. 124 | 125 | ## Usage 126 | 127 | ### Commands 128 | 129 | - `qswitch`: Cycle to the next flavour (runs autofix if needed) 130 | - `qswitch apply `: Switch to a specific flavour 131 | - `qswitch apply --current`: Re-apply current flavour configuration 132 | - `qswitch list`: List available flavours (use `--status` for JSON output) 133 | - `qswitch current`: Show current flavour 134 | - `qswitch panel`: Toggle the quick switch panel 135 | - `qswitch reload`: Reload keybinds 136 | - `qswitch switch-keybinds `: Switch only the keybinds for a specific flavour 137 | - `qswitch exp-setup`: Run the initial setup (creates directories, state file, and updates hyprland.conf with autofix) 138 | - `qswitch --help`: Show help 139 | 140 | ### Setup 141 | 142 | When you first run `qswitch`, it will check for setup and run autofix if needed. 143 | 144 | ```bash 145 | qswitch exp-setup 146 | ``` 147 | 148 | This will: 149 | 150 | 1. Create necessary directories (`~/.config/qswitch`, `~/.cache/qswitch`) 151 | 2. Create the state file `~/.switch_state` 152 | 3. Generate a default `~/.config/qswitch/qswitch.conf` 153 | 4. Append `source=~/.cache/qswitch/qswitch.conf` to your `~/.config/hypr/hyprland.conf` (if not already present) 154 | 5. Remove any incorrect source lines (e.g., from old cache paths) 155 | 156 | You can force the setup (even if files exist) with: 157 | 158 | ```bash 159 | qswitch exp-setup --force 160 | ``` 161 | 162 | The autofix feature automatically detects and fixes common configuration issues. 163 | 164 | ### Examples 165 | 166 | ```bash 167 | # Cycle flavours 168 | qswitch 169 | 170 | # Switch to caelestia 171 | qswitch apply caelestia 172 | 173 | # Re-apply current flavour 174 | qswitch apply --current 175 | 176 | # List flavours 177 | qswitch list 178 | 179 | # Show current flavour 180 | qswitch current 181 | 182 | # Toggle panel 183 | qswitch panel 184 | 185 | # Reload keybinds 186 | qswitch reload 187 | ``` 188 | 189 | ### Shell Completions 190 | 191 | Install completions for bash, zsh, or fish by sourcing the files in `completions/` or using the installed versions. 192 | 193 | ## Files 194 | 195 | - `~/.switch_state`: Stores the current flavour 196 | - `~/.config/qswitch/config.json`: Configuration 197 | - `~/.config/qswitch/keybinds/`: Keybind files 198 | - `~/.cache/qswitch/qswitch.conf`: Generated keybinds (sourced in hyprland.conf) 199 | - `/etc/xdg/quickshell/qswitch/shell.qml`: Panel QML file 200 | -------------------------------------------------------------------------------- /quickshell/icons/noctalia.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 18 | 22 | 26 | 30 | 31 | 32 | 33 | 37 | 41 | 42 | 46 | 53 | 57 | 61 | 65 | 66 | -------------------------------------------------------------------------------- /completions/qswitch.zsh: -------------------------------------------------------------------------------- 1 | #compdef qswitch 2 | compdef _qswitch qswitch 3 | 4 | # zsh completion for qswitch -*- shell-script -*- 5 | 6 | __qswitch_debug() 7 | { 8 | local file="$BASH_COMP_DEBUG_FILE" 9 | if [[ -n ${file} ]]; then 10 | echo "$*" >> "${file}" 11 | fi 12 | } 13 | 14 | _qswitch() 15 | { 16 | local shellCompDirectiveError=1 17 | local shellCompDirectiveNoSpace=2 18 | local shellCompDirectiveNoFileComp=4 19 | local shellCompDirectiveFilterFileExt=8 20 | local shellCompDirectiveFilterDirs=16 21 | local shellCompDirectiveKeepOrder=32 22 | 23 | local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder 24 | local -a completions 25 | 26 | __qswitch_debug "\n========= starting completion logic ==========" 27 | __qswitch_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" 28 | 29 | # The user could have moved the cursor backwards on the command-line. 30 | # We need to trigger completion from the $CURRENT location, so we need 31 | # to truncate the command-line ($words) up to the $CURRENT location. 32 | # (We cannot use $CURSOR as its value does not work when a command is an alias.) 33 | words=("${=words[1,CURRENT]}") 34 | __qswitch_debug "Truncated words[*]: ${words[*]}," 35 | 36 | lastParam=${words[-1]} 37 | lastChar=${lastParam[-1]} 38 | __qswitch_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" 39 | 40 | # For zsh, when completing a flag with an = (e.g., qswitch -n=) 41 | # completions must be prefixed with the flag 42 | setopt local_options BASH_REMATCH 43 | if [[ "${lastParam}" =~ '-.*=' ]]; then 44 | # We are dealing with a flag with an = 45 | flagPrefix="-P ${BASH_REMATCH}" 46 | fi 47 | 48 | # Prepare the command to obtain completions 49 | requestComp="${words[1]} __complete ${words[2,-1]}" 50 | if [ "${lastChar}" = "" ]; then 51 | # If the last parameter is complete (there is a space following it) 52 | # We add an extra empty parameter so we can indicate this to the go completion code. 53 | __qswitch_debug "Adding extra empty parameter" 54 | requestComp="${requestComp} \"\"" 55 | fi 56 | 57 | __qswitch_debug "About to call: eval ${requestComp}" 58 | 59 | # Use eval to handle any environment variables and such 60 | out=$(eval ${requestComp} 2>/dev/null) 61 | __qswitch_debug "completion output: ${out}" 62 | 63 | # Extract the directive integer following a : from the last line 64 | local lastLine 65 | while IFS='\n' read -r line; do 66 | lastLine=${line} 67 | done < <(printf "%s\n" "${out[@]}") 68 | __qswitch_debug "last line: ${lastLine}" 69 | 70 | if [ "${lastLine[1]}" = : ]; then 71 | directive=${lastLine[2,-1]} 72 | # Remove the directive including the : and the newline 73 | local suffix 74 | (( suffix=${#lastLine}+2)) 75 | out=${out[1,-$suffix]} 76 | else 77 | # There is no directive specified. Leave $out as is. 78 | __qswitch_debug "No directive found. Setting do default" 79 | directive=0 80 | fi 81 | 82 | __qswitch_debug "directive: ${directive}" 83 | __qswitch_debug "completions: ${out}" 84 | __qswitch_debug "flagPrefix: ${flagPrefix}" 85 | 86 | if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then 87 | __qswitch_debug "Completion received error. Ignoring completions." 88 | return 89 | fi 90 | 91 | local activeHelpMarker="_activeHelp_ " 92 | local endIndex=${#activeHelpMarker} 93 | local startIndex=$((${#activeHelpMarker}+1)) 94 | local hasActiveHelp=0 95 | while IFS='\n' read -r comp; do 96 | # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) 97 | if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then 98 | __qswitch_debug "ActiveHelp found: $comp" 99 | comp="${comp[$startIndex,-1]}" 100 | if [ -n "$comp" ]; then 101 | compadd -x "${comp}" 102 | __qswitch_debug "ActiveHelp will need delimiter" 103 | hasActiveHelp=1 104 | fi 105 | 106 | continue 107 | fi 108 | 109 | if [ -n "$comp" ]; then 110 | # If requested, completions are returned with a description. 111 | # The description is preceded by a TAB character. 112 | # For zsh's _describe, we need to use a : instead of a TAB. 113 | # We first need to escape any : as part of the completion itself. 114 | comp=${comp//:/\\:} 115 | 116 | local tab="$(printf '\t')" 117 | comp=${comp//$tab/:} 118 | 119 | __qswitch_debug "Adding completion: ${comp}" 120 | completions+=${comp} 121 | lastComp=$comp 122 | fi 123 | done < <(printf "%s\n" "${out[@]}") 124 | 125 | # Add a delimiter after the activeHelp statements, but only if: 126 | # - there are completions following the activeHelp statements, or 127 | # - file completion will be performed (so there will be choices after the activeHelp) 128 | if [ $hasActiveHelp -eq 1 ]; then 129 | if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then 130 | __qswitch_debug "Adding activeHelp delimiter" 131 | compadd -x "--" 132 | hasActiveHelp=0 133 | fi 134 | fi 135 | 136 | if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then 137 | __qswitch_debug "Activating nospace." 138 | noSpace="-S ''" 139 | fi 140 | 141 | if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then 142 | __qswitch_debug "Activating keep order." 143 | keepOrder="-V" 144 | fi 145 | 146 | if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then 147 | # File extension filtering 148 | local filteringCmd 149 | filteringCmd='_files' 150 | for filter in ${completions[@]}; do 151 | if [ ${filter[1]} != '*' ]; then 152 | # zsh requires a glob pattern to do file filtering 153 | filter="\*.$filter" 154 | fi 155 | filteringCmd+=" -g $filter" 156 | done 157 | filteringCmd+=" ${flagPrefix}" 158 | 159 | __qswitch_debug "File filtering command: $filteringCmd" 160 | _arguments '*:filename:'"$filteringCmd" 161 | elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then 162 | # File completion for directories only 163 | local subdir 164 | subdir="${completions[1]}" 165 | if [ -n "$subdir" ]; then 166 | __qswitch_debug "Listing directories in $subdir" 167 | pushd "${subdir}" >/dev/null 2>&1 168 | else 169 | __qswitch_debug "Listing directories in ." 170 | fi 171 | 172 | local result 173 | _arguments '*:dirname:_files -/'" ${flagPrefix}" 174 | result=$? 175 | if [ -n "$subdir" ]; then 176 | popd >/dev/null 2>&1 177 | fi 178 | return $result 179 | else 180 | __qswitch_debug "Calling _describe" 181 | if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then 182 | __qswitch_debug "_describe found some completions" 183 | 184 | # Return the success of having called _describe 185 | return 0 186 | else 187 | __qswitch_debug "_describe did not find completions." 188 | __qswitch_debug "Checking if we should do file completion." 189 | if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then 190 | __qswitch_debug "deactivating file completion" 191 | 192 | # We must return an error code here to let zsh know that there were no 193 | # completions found by _describe; this is what will trigger other 194 | # matching algorithms to attempt to find completions. 195 | # For example zsh can match letters in the middle of words. 196 | return 1 197 | else 198 | # Perform file completion 199 | __qswitch_debug "Activating file completion" 200 | 201 | # We must return the result of this command, so it must be the 202 | # last command, or else we must store its result to return it. 203 | _arguments '*:filename:_files'" ${flagPrefix}" 204 | fi 205 | fi 206 | fi 207 | } 208 | 209 | # don't run the completion function when being source-ed or eval-ed 210 | if [ "$funcstack[1]" = "_qswitch" ]; then 211 | _qswitch 212 | fi 213 | -------------------------------------------------------------------------------- /completions/qswitch.fish: -------------------------------------------------------------------------------- 1 | # fish completion for qswitch -*- shell-script -*- 2 | 3 | function __qswitch_debug 4 | set -l file "$BASH_COMP_DEBUG_FILE" 5 | if test -n "$file" 6 | echo "$argv" >> $file 7 | end 8 | end 9 | 10 | function __qswitch_perform_completion 11 | __qswitch_debug "Starting __qswitch_perform_completion" 12 | 13 | # Extract all args except the last one 14 | set -l args (commandline -opc) 15 | # Extract the last arg and escape it in case it is a space 16 | set -l lastArg (string escape -- (commandline -ct)) 17 | 18 | __qswitch_debug "args: $args" 19 | __qswitch_debug "last arg: $lastArg" 20 | 21 | # Disable ActiveHelp which is not supported for fish shell 22 | set -l requestComp "QSWITCH_ACTIVE_HELP=0 $args[1] __complete $args[2..-1] $lastArg" 23 | 24 | __qswitch_debug "Calling $requestComp" 25 | set -l results (eval $requestComp 2> /dev/null) 26 | 27 | # Some programs may output extra empty lines after the directive. 28 | # Let's ignore them or else it will break completion. 29 | # Ref: https://github.com/spf13/cobra/issues/1279 30 | for line in $results[-1..1] 31 | if test (string trim -- $line) = "" 32 | # Found an empty line, remove it 33 | set results $results[1..-2] 34 | else 35 | # Found non-empty line, we have our proper output 36 | break 37 | end 38 | end 39 | 40 | set -l comps $results[1..-2] 41 | set -l directiveLine $results[-1] 42 | 43 | # For Fish, when completing a flag with an = (e.g., -n=) 44 | # completions must be prefixed with the flag 45 | set -l flagPrefix (string match -r -- '-.*=' "$lastArg") 46 | 47 | __qswitch_debug "Comps: $comps" 48 | __qswitch_debug "DirectiveLine: $directiveLine" 49 | __qswitch_debug "flagPrefix: $flagPrefix" 50 | 51 | for comp in $comps 52 | printf "%s%s\n" "$flagPrefix" "$comp" 53 | end 54 | 55 | printf "%s\n" "$directiveLine" 56 | end 57 | 58 | # this function limits calls to __qswitch_perform_completion, by caching the result behind $__qswitch_perform_completion_once_result 59 | function __qswitch_perform_completion_once 60 | __qswitch_debug "Starting __qswitch_perform_completion_once" 61 | 62 | if test -n "$__qswitch_perform_completion_once_result" 63 | __qswitch_debug "Seems like a valid result already exists, skipping __qswitch_perform_completion" 64 | return 0 65 | end 66 | 67 | set --global __qswitch_perform_completion_once_result (__qswitch_perform_completion) 68 | if test -z "$__qswitch_perform_completion_once_result" 69 | __qswitch_debug "No completions, probably due to a failure" 70 | return 1 71 | end 72 | 73 | __qswitch_debug "Performed completions and set __qswitch_perform_completion_once_result" 74 | return 0 75 | end 76 | 77 | # this function is used to clear the $__qswitch_perform_completion_once_result variable after completions are run 78 | function __qswitch_clear_perform_completion_once_result 79 | __qswitch_debug "" 80 | __qswitch_debug "========= clearing previously set __qswitch_perform_completion_once_result variable ==========" 81 | set --erase __qswitch_perform_completion_once_result 82 | __qswitch_debug "Successfully erased the variable __qswitch_perform_completion_once_result" 83 | end 84 | 85 | function __qswitch_requires_order_preservation 86 | __qswitch_debug "" 87 | __qswitch_debug "========= checking if order preservation is required ==========" 88 | 89 | __qswitch_perform_completion_once 90 | if test -z "$__qswitch_perform_completion_once_result" 91 | __qswitch_debug "Error determining if order preservation is required" 92 | return 1 93 | end 94 | 95 | set -l directive (string sub --start 2 $__qswitch_perform_completion_once_result[-1]) 96 | __qswitch_debug "Directive is: $directive" 97 | 98 | set -l shellCompDirectiveKeepOrder 32 99 | set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2) 100 | __qswitch_debug "Keeporder is: $keeporder" 101 | 102 | if test $keeporder -ne 0 103 | __qswitch_debug "This does require order preservation" 104 | return 0 105 | end 106 | 107 | __qswitch_debug "This doesn't require order preservation" 108 | return 1 109 | end 110 | 111 | 112 | # This function does two things: 113 | # - Obtain the completions and store them in the global __qswitch_comp_results 114 | # - Return false if file completion should be performed 115 | function __qswitch_prepare_completions 116 | __qswitch_debug "" 117 | __qswitch_debug "========= starting completion logic ==========" 118 | 119 | # Start fresh 120 | set --erase __qswitch_comp_results 121 | 122 | __qswitch_perform_completion_once 123 | __qswitch_debug "Completion results: $__qswitch_perform_completion_once_result" 124 | 125 | if test -z "$__qswitch_perform_completion_once_result" 126 | __qswitch_debug "No completion, probably due to a failure" 127 | # Might as well do file completion, in case it helps 128 | return 1 129 | end 130 | 131 | set -l directive (string sub --start 2 $__qswitch_perform_completion_once_result[-1]) 132 | set --global __qswitch_comp_results $__qswitch_perform_completion_once_result[1..-2] 133 | 134 | __qswitch_debug "Completions are: $__qswitch_comp_results" 135 | __qswitch_debug "Directive is: $directive" 136 | 137 | set -l shellCompDirectiveError 1 138 | set -l shellCompDirectiveNoSpace 2 139 | set -l shellCompDirectiveNoFileComp 4 140 | set -l shellCompDirectiveFilterFileExt 8 141 | set -l shellCompDirectiveFilterDirs 16 142 | 143 | if test -z "$directive" 144 | set directive 0 145 | end 146 | 147 | set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) 148 | if test $compErr -eq 1 149 | __qswitch_debug "Received error directive: aborting." 150 | # Might as well do file completion, in case it helps 151 | return 1 152 | end 153 | 154 | set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) 155 | set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) 156 | if test $filefilter -eq 1; or test $dirfilter -eq 1 157 | __qswitch_debug "File extension filtering or directory filtering not supported" 158 | # Do full file completion instead 159 | return 1 160 | end 161 | 162 | set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) 163 | set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) 164 | 165 | __qswitch_debug "nospace: $nospace, nofiles: $nofiles" 166 | 167 | # If we want to prevent a space, or if file completion is NOT disabled, 168 | # we need to count the number of valid completions. 169 | # To do so, we will filter on prefix as the completions we have received 170 | # may not already be filtered so as to allow fish to match on different 171 | # criteria than the prefix. 172 | if test $nospace -ne 0; or test $nofiles -eq 0 173 | set -l prefix (commandline -t | string escape --style=regex) 174 | __qswitch_debug "prefix: $prefix" 175 | 176 | set -l completions (string match -r -- "^$prefix.*" $__qswitch_comp_results) 177 | set --global __qswitch_comp_results $completions 178 | __qswitch_debug "Filtered completions are: $__qswitch_comp_results" 179 | 180 | # Important not to quote the variable for count to work 181 | set -l numComps (count $__qswitch_comp_results) 182 | __qswitch_debug "numComps: $numComps" 183 | 184 | if test $numComps -eq 1; and test $nospace -ne 0 185 | # We must first split on \t to get rid of the descriptions to be 186 | # able to check what the actual completion will be. 187 | # We don't need descriptions anyway since there is only a single 188 | # real completion which the shell will expand immediately. 189 | set -l split (string split --max 1 \t $__qswitch_comp_results[1]) 190 | 191 | # Fish won't add a space if the completion ends with any 192 | # of the following characters: @=/:., 193 | set -l lastChar (string sub -s -1 -- $split) 194 | if not string match -r -q "[@=/:.,]" -- "$lastChar" 195 | # In other cases, to support the "nospace" directive we trick the shell 196 | # by outputting an extra, longer completion. 197 | __qswitch_debug "Adding second completion to perform nospace directive" 198 | set --global __qswitch_comp_results $split[1] $split[1]. 199 | __qswitch_debug "Completions are now: $__qswitch_comp_results" 200 | end 201 | end 202 | 203 | if test $numComps -eq 0; and test $nofiles -eq 0 204 | # To be consistent with bash and zsh, we only trigger file 205 | # completion when there are no other completions 206 | __qswitch_debug "Requesting file completion" 207 | return 1 208 | end 209 | end 210 | 211 | return 0 212 | end 213 | 214 | # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves 215 | # so we can properly delete any completions provided by another script. 216 | # Only do this if the program can be found, or else fish may print some errors; besides, 217 | # the existing completions will only be loaded if the program can be found. 218 | if type -q "qswitch" 219 | # The space after the program name is essential to trigger completion for the program 220 | # and not completion of the program name itself. 221 | # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. 222 | complete --do-complete "qswitch " > /dev/null 2>&1 223 | end 224 | 225 | # Remove any pre-existing completions for the program since we will be handling all of them. 226 | complete -c qswitch -e 227 | 228 | # this will get called after the two calls below and clear the $__qswitch_perform_completion_once_result global 229 | complete -c qswitch -n '__qswitch_clear_perform_completion_once_result' 230 | # The call to __qswitch_prepare_completions will setup __qswitch_comp_results 231 | # which provides the program's completion choices. 232 | # If this doesn't require order preservation, we don't use the -k flag 233 | complete -c qswitch -n 'not __qswitch_requires_order_preservation && __qswitch_prepare_completions' -f -a '$__qswitch_comp_results' 234 | # otherwise we use the -k flag 235 | complete -k -c qswitch -n '__qswitch_requires_order_preservation && __qswitch_prepare_completions' -f -a '$__qswitch_comp_results' 236 | -------------------------------------------------------------------------------- /quickshell/icons/dms.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 17 | 18 | 19 | 20 | 24 | 27 | 31 | 32 | 35 | 39 | 40 | 43 | 47 | 48 | 51 | 55 | 56 | 59 | 63 | 64 | 67 | 71 | 72 | 73 | 76 | 82 | 88 | 94 | 100 | 106 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /completions/qswitch.bash: -------------------------------------------------------------------------------- 1 | # bash completion V2 for qswitch -*- shell-script -*- 2 | 3 | __qswitch_debug() 4 | { 5 | if [[ -n ${BASH_COMP_DEBUG_FILE-} ]]; then 6 | echo "$*" >> "${BASH_COMP_DEBUG_FILE}" 7 | fi 8 | } 9 | 10 | # Macs have bash3 for which the bash-completion package doesn't include 11 | # _init_completion. This is a minimal version of that function. 12 | __qswitch_init_completion() 13 | { 14 | COMPREPLY=() 15 | _get_comp_words_by_ref "$@" cur prev words cword 16 | } 17 | 18 | # This function calls the qswitch program to obtain the completion 19 | # results and the directive. It fills the 'out' and 'directive' vars. 20 | __qswitch_get_completion_results() { 21 | local requestComp lastParam lastChar args 22 | 23 | # Prepare the command to request completions for the program. 24 | # Calling ${words[0]} instead of directly qswitch allows handling aliases 25 | args=("${words[@]:1}") 26 | requestComp="${words[0]} __complete ${args[*]}" 27 | 28 | lastParam=${words[$((${#words[@]}-1))]} 29 | lastChar=${lastParam:$((${#lastParam}-1)):1} 30 | __qswitch_debug "lastParam ${lastParam}, lastChar ${lastChar}" 31 | 32 | if [[ -z ${cur} && ${lastChar} != = ]]; then 33 | # If the last parameter is complete (there is a space following it) 34 | # We add an extra empty parameter so we can indicate this to the go method. 35 | __qswitch_debug "Adding extra empty parameter" 36 | requestComp="${requestComp} ''" 37 | fi 38 | 39 | # When completing a flag with an = (e.g., qswitch -n=) 40 | # bash focuses on the part after the =, so we need to remove 41 | # the flag part from $cur 42 | if [[ ${cur} == -*=* ]]; then 43 | cur="${cur#*=}" 44 | fi 45 | 46 | __qswitch_debug "Calling ${requestComp}" 47 | # Use eval to handle any environment variables and such 48 | out=$(eval "${requestComp}" 2>/dev/null) 49 | 50 | # Extract the directive integer at the very end of the output following a colon (:) 51 | directive=${out##*:} 52 | # Remove the directive 53 | out=${out%:*} 54 | if [[ ${directive} == "${out}" ]]; then 55 | # There is not directive specified 56 | directive=0 57 | fi 58 | __qswitch_debug "The completion directive is: ${directive}" 59 | __qswitch_debug "The completions are: ${out}" 60 | } 61 | 62 | __qswitch_process_completion_results() { 63 | local shellCompDirectiveError=1 64 | local shellCompDirectiveNoSpace=2 65 | local shellCompDirectiveNoFileComp=4 66 | local shellCompDirectiveFilterFileExt=8 67 | local shellCompDirectiveFilterDirs=16 68 | local shellCompDirectiveKeepOrder=32 69 | 70 | if (((directive & shellCompDirectiveError) != 0)); then 71 | # Error code. No completion. 72 | __qswitch_debug "Received error from custom completion go code" 73 | return 74 | else 75 | if (((directive & shellCompDirectiveNoSpace) != 0)); then 76 | if [[ $(type -t compopt) == builtin ]]; then 77 | __qswitch_debug "Activating no space" 78 | compopt -o nospace 79 | else 80 | __qswitch_debug "No space directive not supported in this version of bash" 81 | fi 82 | fi 83 | if (((directive & shellCompDirectiveKeepOrder) != 0)); then 84 | if [[ $(type -t compopt) == builtin ]]; then 85 | # no sort isn't supported for bash less than < 4.4 86 | if [[ ${BASH_VERSINFO[0]} -lt 4 || ( ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 4 ) ]]; then 87 | __qswitch_debug "No sort directive not supported in this version of bash" 88 | else 89 | __qswitch_debug "Activating keep order" 90 | compopt -o nosort 91 | fi 92 | else 93 | __qswitch_debug "No sort directive not supported in this version of bash" 94 | fi 95 | fi 96 | if (((directive & shellCompDirectiveNoFileComp) != 0)); then 97 | if [[ $(type -t compopt) == builtin ]]; then 98 | __qswitch_debug "Activating no file completion" 99 | compopt +o default 100 | else 101 | __qswitch_debug "No file completion directive not supported in this version of bash" 102 | fi 103 | fi 104 | fi 105 | 106 | # Separate activeHelp from normal completions 107 | local completions=() 108 | local activeHelp=() 109 | __qswitch_extract_activeHelp 110 | 111 | if (((directive & shellCompDirectiveFilterFileExt) != 0)); then 112 | # File extension filtering 113 | local fullFilter="" filter filteringCmd 114 | 115 | # Do not use quotes around the $completions variable or else newline 116 | # characters will be kept. 117 | for filter in ${completions[*]}; do 118 | fullFilter+="$filter|" 119 | done 120 | 121 | filteringCmd="_filedir $fullFilter" 122 | __qswitch_debug "File filtering command: $filteringCmd" 123 | $filteringCmd 124 | elif (((directive & shellCompDirectiveFilterDirs) != 0)); then 125 | # File completion for directories only 126 | 127 | local subdir 128 | subdir=${completions[0]} 129 | if [[ -n $subdir ]]; then 130 | __qswitch_debug "Listing directories in $subdir" 131 | pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return 132 | else 133 | __qswitch_debug "Listing directories in ." 134 | _filedir -d 135 | fi 136 | else 137 | __qswitch_handle_completion_types 138 | fi 139 | 140 | __qswitch_handle_special_char "$cur" : 141 | __qswitch_handle_special_char "$cur" = 142 | 143 | # Print the activeHelp statements before we finish 144 | __qswitch_handle_activeHelp 145 | } 146 | 147 | __qswitch_handle_activeHelp() { 148 | # Print the activeHelp statements 149 | if ((${#activeHelp[*]} != 0)); then 150 | if [ -z $COMP_TYPE ]; then 151 | # Bash v3 does not set the COMP_TYPE variable. 152 | printf "\n"; 153 | printf "%s\n" "${activeHelp[@]}" 154 | printf "\n" 155 | __qswitch_reprint_commandLine 156 | return 157 | fi 158 | 159 | # Only print ActiveHelp on the second TAB press 160 | if [ $COMP_TYPE -eq 63 ]; then 161 | printf "\n" 162 | printf "%s\n" "${activeHelp[@]}" 163 | 164 | if ((${#COMPREPLY[*]} == 0)); then 165 | # When there are no completion choices from the program, file completion 166 | # may kick in if the program has not disabled it; in such a case, we want 167 | # to know if any files will match what the user typed, so that we know if 168 | # there will be completions presented, so that we know how to handle ActiveHelp. 169 | # To find out, we actually trigger the file completion ourselves; 170 | # the call to _filedir will fill COMPREPLY if files match. 171 | if (((directive & shellCompDirectiveNoFileComp) == 0)); then 172 | __qswitch_debug "Listing files" 173 | _filedir 174 | fi 175 | fi 176 | 177 | if ((${#COMPREPLY[*]} != 0)); then 178 | # If there are completion choices to be shown, print a delimiter. 179 | # Re-printing the command-line will automatically be done 180 | # by the shell when it prints the completion choices. 181 | printf -- "--" 182 | else 183 | # When there are no completion choices at all, we need 184 | # to re-print the command-line since the shell will 185 | # not be doing it itself. 186 | __qswitch_reprint_commandLine 187 | fi 188 | elif [ $COMP_TYPE -eq 37 ] || [ $COMP_TYPE -eq 42 ]; then 189 | # For completion type: menu-complete/menu-complete-backward and insert-completions 190 | # the completions are immediately inserted into the command-line, so we first 191 | # print the activeHelp message and reprint the command-line since the shell won't. 192 | printf "\n" 193 | printf "%s\n" "${activeHelp[@]}" 194 | 195 | __qswitch_reprint_commandLine 196 | fi 197 | fi 198 | } 199 | 200 | __qswitch_reprint_commandLine() { 201 | # The prompt format is only available from bash 4.4. 202 | # We test if it is available before using it. 203 | if (x=${PS1@P}) 2> /dev/null; then 204 | printf "%s" "${PS1@P}${COMP_LINE[@]}" 205 | else 206 | # Can't print the prompt. Just print the 207 | # text the user had typed, it is workable enough. 208 | printf "%s" "${COMP_LINE[@]}" 209 | fi 210 | } 211 | 212 | # Separate activeHelp lines from real completions. 213 | # Fills the $activeHelp and $completions arrays. 214 | __qswitch_extract_activeHelp() { 215 | local activeHelpMarker="_activeHelp_ " 216 | local endIndex=${#activeHelpMarker} 217 | 218 | while IFS='' read -r comp; do 219 | [[ -z $comp ]] && continue 220 | 221 | if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then 222 | comp=${comp:endIndex} 223 | __qswitch_debug "ActiveHelp found: $comp" 224 | if [[ -n $comp ]]; then 225 | activeHelp+=("$comp") 226 | fi 227 | else 228 | # Not an activeHelp line but a normal completion 229 | completions+=("$comp") 230 | fi 231 | done <<<"${out}" 232 | } 233 | 234 | __qswitch_handle_completion_types() { 235 | __qswitch_debug "__qswitch_handle_completion_types: COMP_TYPE is $COMP_TYPE" 236 | 237 | case $COMP_TYPE in 238 | 37|42) 239 | # Type: menu-complete/menu-complete-backward and insert-completions 240 | # If the user requested inserting one completion at a time, or all 241 | # completions at once on the command-line we must remove the descriptions. 242 | # https://github.com/spf13/cobra/issues/1508 243 | 244 | # If there are no completions, we don't need to do anything 245 | (( ${#completions[@]} == 0 )) && return 0 246 | 247 | local tab=$'\t' 248 | 249 | # Strip any description and escape the completion to handled special characters 250 | IFS=$'\n' read -ra completions -d '' < <(printf "%q\n" "${completions[@]%%$tab*}") 251 | 252 | # Only consider the completions that match 253 | IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n'; compgen -W "${completions[*]}" -- "${cur}") 254 | 255 | # compgen looses the escaping so we need to escape all completions again since they will 256 | # all be inserted on the command-line. 257 | IFS=$'\n' read -ra COMPREPLY -d '' < <(printf "%q\n" "${COMPREPLY[@]}") 258 | ;; 259 | 260 | *) 261 | # Type: complete (normal completion) 262 | __qswitch_handle_standard_completion_case 263 | ;; 264 | esac 265 | } 266 | 267 | __qswitch_handle_standard_completion_case() { 268 | local tab=$'\t' 269 | 270 | # If there are no completions, we don't need to do anything 271 | (( ${#completions[@]} == 0 )) && return 0 272 | 273 | # Short circuit to optimize if we don't have descriptions 274 | if [[ "${completions[*]}" != *$tab* ]]; then 275 | # First, escape the completions to handle special characters 276 | IFS=$'\n' read -ra completions -d '' < <(printf "%q\n" "${completions[@]}") 277 | # Only consider the completions that match what the user typed 278 | IFS=$'\n' read -ra COMPREPLY -d '' < <(IFS=$'\n'; compgen -W "${completions[*]}" -- "${cur}") 279 | 280 | # compgen looses the escaping so, if there is only a single completion, we need to 281 | # escape it again because it will be inserted on the command-line. If there are multiple 282 | # completions, we don't want to escape them because they will be printed in a list 283 | # and we don't want to show escape characters in that list. 284 | if (( ${#COMPREPLY[@]} == 1 )); then 285 | COMPREPLY[0]=$(printf "%q" "${COMPREPLY[0]}") 286 | fi 287 | return 0 288 | fi 289 | 290 | local longest=0 291 | local compline 292 | # Look for the longest completion so that we can format things nicely 293 | while IFS='' read -r compline; do 294 | [[ -z $compline ]] && continue 295 | 296 | # Before checking if the completion matches what the user typed, 297 | # we need to strip any description and escape the completion to handle special 298 | # characters because those escape characters are part of what the user typed. 299 | # Don't call "printf" in a sub-shell because it will be much slower 300 | # since we are in a loop. 301 | printf -v comp "%q" "${compline%%$tab*}" &>/dev/null || comp=$(printf "%q" "${compline%%$tab*}") 302 | 303 | # Only consider the completions that match 304 | [[ $comp == "$cur"* ]] || continue 305 | 306 | # The completions matches. Add it to the list of full completions including 307 | # its description. We don't escape the completion because it may get printed 308 | # in a list if there are more than one and we don't want show escape characters 309 | # in that list. 310 | COMPREPLY+=("$compline") 311 | 312 | # Strip any description before checking the length, and again, don't escape 313 | # the completion because this length is only used when printing the completions 314 | # in a list and we don't want show escape characters in that list. 315 | comp=${compline%%$tab*} 316 | if ((${#comp}>longest)); then 317 | longest=${#comp} 318 | fi 319 | done < <(printf "%s\n" "${completions[@]}") 320 | 321 | # If there is a single completion left, remove the description text and escape any special characters 322 | if ((${#COMPREPLY[*]} == 1)); then 323 | __qswitch_debug "COMPREPLY[0]: ${COMPREPLY[0]}" 324 | COMPREPLY[0]=$(printf "%q" "${COMPREPLY[0]%%$tab*}") 325 | __qswitch_debug "Removed description from single completion, which is now: ${COMPREPLY[0]}" 326 | else 327 | # Format the descriptions 328 | __qswitch_format_comp_descriptions $longest 329 | fi 330 | } 331 | 332 | __qswitch_handle_special_char() 333 | { 334 | local comp="$1" 335 | local char=$2 336 | if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then 337 | local word=${comp%"${comp##*${char}}"} 338 | local idx=${#COMPREPLY[*]} 339 | while ((--idx >= 0)); do 340 | COMPREPLY[idx]=${COMPREPLY[idx]#"$word"} 341 | done 342 | fi 343 | } 344 | 345 | __qswitch_format_comp_descriptions() 346 | { 347 | local tab=$'\t' 348 | local comp desc maxdesclength 349 | local longest=$1 350 | 351 | local i ci 352 | for ci in ${!COMPREPLY[*]}; do 353 | comp=${COMPREPLY[ci]} 354 | # Properly format the description string which follows a tab character if there is one 355 | if [[ "$comp" == *$tab* ]]; then 356 | __qswitch_debug "Original comp: $comp" 357 | desc=${comp#*$tab} 358 | comp=${comp%%$tab*} 359 | 360 | # $COLUMNS stores the current shell width. 361 | # Remove an extra 4 because we add 2 spaces and 2 parentheses. 362 | maxdesclength=$(( COLUMNS - longest - 4 )) 363 | 364 | # Make sure we can fit a description of at least 8 characters 365 | # if we are to align the descriptions. 366 | if ((maxdesclength > 8)); then 367 | # Add the proper number of spaces to align the descriptions 368 | for ((i = ${#comp} ; i < longest ; i++)); do 369 | comp+=" " 370 | done 371 | else 372 | # Don't pad the descriptions so we can fit more text after the completion 373 | maxdesclength=$(( COLUMNS - ${#comp} - 4 )) 374 | fi 375 | 376 | # If there is enough space for any description text, 377 | # truncate the descriptions that are too long for the shell width 378 | if ((maxdesclength > 0)); then 379 | if ((${#desc} > maxdesclength)); then 380 | desc=${desc:0:$(( maxdesclength - 1 ))} 381 | desc+="…" 382 | fi 383 | comp+=" ($desc)" 384 | fi 385 | COMPREPLY[ci]=$comp 386 | __qswitch_debug "Final comp: $comp" 387 | fi 388 | done 389 | } 390 | 391 | __start_qswitch() 392 | { 393 | local cur prev words cword split 394 | 395 | COMPREPLY=() 396 | 397 | # Call _init_completion from the bash-completion package 398 | # to prepare the arguments properly 399 | if declare -F _init_completion >/dev/null 2>&1; then 400 | _init_completion -n =: || return 401 | else 402 | __qswitch_init_completion -n =: || return 403 | fi 404 | 405 | __qswitch_debug 406 | __qswitch_debug "========= starting completion logic ==========" 407 | __qswitch_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" 408 | 409 | # The user could have moved the cursor backwards on the command-line. 410 | # We need to trigger completion from the $cword location, so we need 411 | # to truncate the command-line ($words) up to the $cword location. 412 | words=("${words[@]:0:$cword+1}") 413 | __qswitch_debug "Truncated words[*]: ${words[*]}," 414 | 415 | local out directive 416 | __qswitch_get_completion_results 417 | __qswitch_process_completion_results 418 | } 419 | 420 | if [[ $(type -t compopt) = "builtin" ]]; then 421 | complete -o default -F __start_qswitch qswitch 422 | else 423 | complete -o default -o nospace -F __start_qswitch qswitch 424 | fi 425 | 426 | # ex: ts=4 sw=4 et filetype=sh 427 | -------------------------------------------------------------------------------- /quickshell/shell.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2 | import QtQuick.Layouts 3 | import QtQuick.Controls 4 | import Quickshell 5 | import Quickshell.Io 6 | import Quickshell.Wayland 7 | import QtQuick.Effects 8 | 9 | // pragma ComponentBehavior: Bound 10 | 11 | Scope { 12 | id: root 13 | 14 | // --- CATPPUCCIN MOCHA THEME --- 15 | property color cBase: "#1e1e2e" 16 | property color cMantle: "#181825" 17 | property color cCrust: "#11111b" 18 | property color cSurface0: "#313244" 19 | property color cSurface1: "#45475a" 20 | property color cSurface2: "#585b70" 21 | property color cOverlay0: "#6c7086" 22 | property color cText: "#cdd6f4" 23 | property color cSubtext0: "#a6adc8" 24 | property color cSubtext1: "#bac2de" 25 | property color cLavender: "#b4befe" 26 | property color cMauve: "#cba6f7" 27 | property color cPink: "#f5c2e7" 28 | property color cRosewater: "#f5e0dc" 29 | property color cRed: "#f38ba8" 30 | property color cYellow: "#f9e2af" 31 | 32 | // Default colors for flavours 33 | property var flavourColors: { 34 | "ii": "#51debd", 35 | "caelestia": "#a6e3a1", 36 | "noctalia-shell": "#a9aefe", 37 | "dms": "#D0BCFF", 38 | "mannu": "#cba6f7", 39 | "ocean": "#94e2d5" 40 | } 41 | property color defaultFlavourColor: "#b4befe" 42 | 43 | // Icons path 44 | property string iconsBasePath: Quickshell.shellPath("icons/") 45 | property var flavourIcons: { 46 | "ii": "ii.svg", 47 | "noctalia-shell": "noctalia.svg", 48 | "caelestia": "pacman.svg", 49 | "mannu": "mannu.svg", 50 | "dms": "dms.svg" 51 | } 52 | 53 | property string currentFlavour: "" 54 | property var flavourInstallStatus: ({}) 55 | property bool showInfoPopup: false 56 | 57 | // 1. Process Handler 58 | Process { 59 | id: switcher 60 | command: [] 61 | onRunningChanged: { 62 | if (running) console.log("Executing switch command..."); 63 | } 64 | onExited: currentFlavourLoader.running = true; 65 | } 66 | 67 | Process { 68 | id: currentFlavourLoader 69 | command: ["qswitch", "current"] 70 | stdout: SplitParser { 71 | onRead: data => root.currentFlavour = data.trim(); 72 | } 73 | } 74 | 75 | Process { 76 | id: flavourLoader 77 | command: ["qswitch", "list", "--status"] 78 | stdout: SplitParser { 79 | onRead: data => { 80 | var trimmed = data.trim(); 81 | if (trimmed !== "" && trimmed.startsWith("[")) { 82 | try { 83 | var flavours = JSON.parse(trimmed); 84 | for (var i = 0; i < flavours.length; i++) { 85 | var f = flavours[i]; 86 | var flavourId = f.name; 87 | var installed = f.installed; 88 | var color = root.flavourColors[flavourId] || root.defaultFlavourColor; 89 | var name = flavourId.charAt(0).toUpperCase() + flavourId.slice(1); 90 | 91 | root.flavourInstallStatus[flavourId] = installed; 92 | 93 | masterModel.append({ 94 | "name": name, 95 | "flavourId": flavourId, 96 | "color": color, 97 | "desc": name + " Theme", 98 | "installed": installed 99 | }); 100 | } 101 | } catch (e) { 102 | console.log("Failed to parse flavour status JSON:", e); 103 | } 104 | } 105 | } 106 | } 107 | onExited: root.updateFilter(); 108 | } 109 | 110 | function setFlavour(flavour) { 111 | switcher.command = ["qswitch", "apply", flavour]; 112 | switcher.running = true; 113 | } 114 | 115 | function updateFilter() { 116 | var searchTerm = searchField.text.toLowerCase(); 117 | displayModel.clear(); 118 | 119 | for (var i = 0; i < masterModel.count; i++) { 120 | var item = masterModel.get(i); 121 | if (searchTerm === "" || item.name.toLowerCase().includes(searchTerm) || item.desc.toLowerCase().includes(searchTerm)) { 122 | displayModel.append(item); 123 | } 124 | } 125 | if (displayModel.count > 0) flavorList.currentIndex = 0; 126 | } 127 | 128 | ListModel { id: masterModel } 129 | ListModel { id: displayModel } 130 | 131 | Component.onCompleted: { 132 | currentFlavourLoader.running = true; 133 | flavourLoader.running = true; 134 | } 135 | 136 | // --- FIXED PANEL WINDOW --- 137 | PanelWindow { 138 | // Quickshell anchors use specific boolean properties for screen edges 139 | anchors { 140 | top: true 141 | bottom: true 142 | left: true 143 | right: true 144 | } 145 | color: "transparent" 146 | 147 | WlrLayershell.keyboardFocus: WlrLayershell.Exclusive 148 | WlrLayershell.layer: WlrLayershell.Overlay 149 | 150 | Rectangle { 151 | id: menuRoot 152 | width: 500 153 | height: 480 154 | anchors.centerIn: parent 155 | color: root.cBase 156 | radius: 20 157 | border.color: Qt.alpha(root.cSurface1, 0.5) 158 | border.width: 1 159 | clip: true 160 | 161 | Rectangle { 162 | anchors.fill: parent; anchors.margins: 1; radius: 19 163 | color: "transparent"; border.color: Qt.alpha(root.cLavender, 0.05); border.width: 1 164 | } 165 | 166 | ParallelAnimation { 167 | running: true 168 | NumberAnimation { target: menuRoot; property: "scale"; from: 0.92; to: 1.0; duration: 300; easing.type: Easing.OutBack; easing.overshoot: 1.2 } 169 | NumberAnimation { target: menuRoot; property: "opacity"; from: 0; to: 1.0; duration: 250; easing.type: Easing.OutQuart } 170 | } 171 | 172 | ColumnLayout { 173 | anchors.fill: parent 174 | anchors.margins: 24 175 | spacing: 20 176 | 177 | // --- HEADER --- 178 | RowLayout { 179 | Layout.fillWidth: true 180 | Layout.preferredHeight: 48 181 | spacing: 14 182 | 183 | Rectangle { 184 | Layout.preferredWidth: 42; Layout.preferredHeight: 42; radius: 12 185 | color: Qt.alpha(root.cSurface0, 0.5) 186 | Image { 187 | anchors.centerIn: parent; width: 32; height: 32 188 | source: "file://" + root.iconsBasePath + "arch.svg" 189 | fillMode: Image.PreserveAspectFit; smooth: true 190 | } 191 | } 192 | 193 | Column { 194 | spacing: 4; Layout.alignment: Qt.AlignVCenter 195 | Text { text: "QuickSwitch"; color: root.cText; font.pixelSize: 18; font.bold: true; font.letterSpacing: 0.5 } 196 | Text { text: "Select a theme to apply"; color: root.cSubtext0; font.pixelSize: 12 } 197 | } 198 | 199 | Item { Layout.fillWidth: true } 200 | 201 | // Info Button 202 | Rectangle { 203 | Layout.preferredWidth: 32; Layout.preferredHeight: 32; radius: 8 204 | color: infoBtnArea.containsMouse ? root.cSurface1 : root.cSurface0 205 | Text { 206 | anchors.centerIn: parent; text: "i" 207 | color: root.cLavender; font.pixelSize: 18; font.bold: true; font.family: "Monospace" 208 | } 209 | MouseArea { 210 | id: infoBtnArea 211 | anchors.fill: parent; hoverEnabled: true 212 | onClicked: root.showInfoPopup = true 213 | } 214 | } 215 | 216 | // Close Button 217 | Rectangle { 218 | Layout.preferredWidth: 32; Layout.preferredHeight: 32; radius: 8 219 | color: closeBtnArea.containsMouse ? root.cSurface1 : root.cSurface0 220 | Text { 221 | anchors.centerIn: parent; text: "×" 222 | color: root.cSubtext0; font.pixelSize: 18; font.bold: true 223 | } 224 | MouseArea { 225 | id: closeBtnArea 226 | anchors.fill: parent; hoverEnabled: true 227 | onClicked: Qt.quit() 228 | } 229 | } 230 | } 231 | 232 | Rectangle { Layout.fillWidth: true; Layout.preferredHeight: 1; color: root.cSurface0 } 233 | 234 | // --- CONTENT AREA --- 235 | Item { 236 | Layout.fillWidth: true 237 | Layout.fillHeight: true 238 | 239 | // 1. Normal List View 240 | ListView { 241 | id: flavorList 242 | anchors.fill: parent 243 | clip: true 244 | spacing: 10 245 | // Show only if there are items to display 246 | visible: displayModel.count > 0 247 | model: displayModel 248 | currentIndex: 0 249 | 250 | ScrollBar.vertical: ScrollBar { 251 | width: 6 252 | policy: ScrollBar.AsNeeded 253 | contentItem: Rectangle { implicitWidth: 6; radius: 3; color: root.cSurface2; opacity: 0.6 } 254 | background: Rectangle { implicitWidth: 6; radius: 3; color: root.cSurface0; opacity: 0.3 } 255 | } 256 | 257 | add: Transition { ParallelAnimation { NumberAnimation { property: "opacity"; from: 0; to: 1.0; duration: 250; easing.type: Easing.OutQuart } NumberAnimation { property: "x"; from: -30; to: 0; duration: 300; easing.type: Easing.OutBack } } } 258 | displaced: Transition { NumberAnimation { properties: "y"; duration: 200; easing.type: Easing.OutQuart } } 259 | 260 | delegate: Rectangle { 261 | id: listDelegate 262 | width: ListView.view.width; height: 72; radius: 14 263 | property bool isSelected: ListView.isCurrentItem 264 | property bool isHovered: mouseArea.containsMouse 265 | property color itemColor: model.color 266 | property bool isActive: model.flavourId === root.currentFlavour 267 | property string flavourIcon: root.flavourIcons[model.flavourId] || "" 268 | property bool hasIcon: flavourIcon !== "" 269 | property bool isInstalled: model.installed !== undefined ? model.installed : true 270 | 271 | color: { 272 | if (!isInstalled) return Qt.alpha(root.cRed, 0.1); 273 | if (isActive) return Qt.alpha(itemColor, 0.25); 274 | if (isSelected) return Qt.alpha(itemColor, 0.15); 275 | if (isHovered) return Qt.alpha(root.cSurface0, 0.6); 276 | return "transparent"; 277 | } 278 | 279 | Behavior on color { ColorAnimation { duration: 180; easing.type: Easing.OutQuart } } 280 | border.color: isActive ? Qt.alpha(itemColor, 0.6) : (isSelected ? Qt.alpha(itemColor, 0.4) : "transparent") 281 | border.width: isActive ? 2 : (isSelected ? 2 : 0) 282 | Behavior on border.width { NumberAnimation { duration: 150 } } 283 | 284 | RowLayout { 285 | anchors.fill: parent; anchors.leftMargin: 16; anchors.rightMargin: 16; spacing: 16 286 | Rectangle { 287 | Layout.preferredWidth: 48; Layout.preferredHeight: 48; radius: 12 288 | color: Qt.alpha(listDelegate.itemColor, 0.15); border.color: Qt.alpha(listDelegate.itemColor, 0.3); border.width: 1 289 | Image { 290 | visible: listDelegate.hasIcon 291 | anchors.centerIn: parent; width: listDelegate.isSelected ? 40 : 36; height: width 292 | source: listDelegate.hasIcon ? "file://" + root.iconsBasePath + listDelegate.flavourIcon : "" 293 | fillMode: Image.PreserveAspectFit; smooth: true 294 | Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutBack } } 295 | } 296 | Rectangle { 297 | visible: !listDelegate.hasIcon 298 | anchors.centerIn: parent; width: listDelegate.isSelected ? 28 : 24; height: width; radius: 8 299 | color: listDelegate.itemColor 300 | Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutBack } } 301 | } 302 | } 303 | ColumnLayout { 304 | Layout.fillWidth: true; Layout.alignment: Qt.AlignVCenter; spacing: 6 305 | RowLayout { 306 | spacing: 8 307 | Text { text: model.name; color: listDelegate.isActive ? root.cText : (listDelegate.isSelected ? root.cText : root.cSubtext1); font.pixelSize: 16; font.bold: true } 308 | Rectangle { visible: listDelegate.isActive && listDelegate.isInstalled; width: 50; height: 20; radius: 10; color: Qt.alpha(listDelegate.itemColor, 0.3); Text { anchors.centerIn: parent; text: "Active"; color: listDelegate.itemColor; font.pixelSize: 10; font.bold: true } } 309 | Rectangle { visible: !listDelegate.isInstalled; width: 85; height: 20; radius: 10; color: Qt.alpha(root.cRed, 0.3); Text { anchors.centerIn: parent; text: "Not Installed"; color: root.cRed; font.pixelSize: 10; font.bold: true } } 310 | } 311 | Text { text: model.desc; color: root.cSubtext0; font.pixelSize: 13; opacity: listDelegate.isSelected ? 0.9 : 0.7 } 312 | } 313 | Text { Layout.preferredWidth: 36; text: listDelegate.isActive ? "✓" : "→"; color: listDelegate.itemColor; font.pixelSize: 18; font.bold: true; horizontalAlignment: Text.AlignHCenter; opacity: listDelegate.isSelected || listDelegate.isHovered || listDelegate.isActive ? 1 : 0 } 314 | } 315 | MouseArea { 316 | id: mouseArea; anchors.fill: parent; hoverEnabled: true; cursorShape: listDelegate.isInstalled ? Qt.PointingHandCursor : Qt.ForbiddenCursor 317 | onClicked: { if (listDelegate.isInstalled) { flavorList.currentIndex = index; root.setFlavour(model.flavourId); } } 318 | } 319 | } 320 | } 321 | 322 | // 2. No Search Results (Master has items, but Display is empty) 323 | ColumnLayout { 324 | anchors.centerIn: parent 325 | visible: displayModel.count === 0 && masterModel.count > 0 326 | spacing: 15 327 | 328 | Text { 329 | text: "🤔" 330 | font.pixelSize: 48 331 | Layout.alignment: Qt.AlignHCenter 332 | } 333 | Text { 334 | text: "No matching themes found" 335 | color: root.cText 336 | font.pixelSize: 16 337 | font.bold: true 338 | Layout.alignment: Qt.AlignHCenter 339 | } 340 | Text { 341 | text: "Try searching for something else" 342 | color: root.cSubtext0 343 | font.pixelSize: 13 344 | Layout.alignment: Qt.AlignHCenter 345 | } 346 | } 347 | 348 | // 3. No Config / No Flavours Found (Master is empty) 349 | ColumnLayout { 350 | anchors.centerIn: parent 351 | // Only show if masterModel is empty and we aren't currently loading data 352 | visible: masterModel.count === 0 && !flavourLoader.running 353 | spacing: 20 354 | 355 | AnimatedImage { 356 | Layout.preferredWidth: 128 357 | Layout.preferredHeight: 128 358 | Layout.alignment: Qt.AlignHCenter 359 | source: "file://" + root.iconsBasePath + "confused.gif" 360 | fillMode: Image.PreserveAspectFit 361 | playing: visible 362 | } 363 | 364 | Text { 365 | text: "Did you forget to add the config?" 366 | color: root.cText 367 | font.pixelSize: 16 368 | font.bold: true 369 | Layout.alignment: Qt.AlignHCenter 370 | } 371 | 372 | // Example Config Button 373 | Rectangle { 374 | Layout.preferredWidth: 160 375 | Layout.preferredHeight: 40 376 | Layout.alignment: Qt.AlignHCenter 377 | radius: 10 378 | color: cfgBtn.containsMouse ? root.cSurface1 : root.cSurface0 379 | border.color: root.cLavender 380 | border.width: 1 381 | 382 | Text { 383 | anchors.centerIn: parent 384 | text: "View Example Config" 385 | color: root.cLavender 386 | font.bold: true 387 | font.pixelSize: 13 388 | } 389 | 390 | MouseArea { 391 | id: cfgBtn 392 | anchors.fill: parent 393 | hoverEnabled: true 394 | cursorShape: Qt.PointingHandCursor 395 | onClicked: Qt.openUrlExternally("https://github.com/MannuVilasara/qswitch/tree/main/example") 396 | } 397 | } 398 | } 399 | } 400 | 401 | // --- SEARCH BAR --- 402 | Rectangle { 403 | Layout.fillWidth: true 404 | Layout.preferredHeight: 52 405 | color: root.cMantle 406 | radius: 14 407 | border.color: searchField.activeFocus ? root.cLavender : root.cSurface0 408 | border.width: searchField.activeFocus ? 2 : 1 409 | Behavior on border.color { ColorAnimation { duration: 200 } } 410 | 411 | RowLayout { 412 | anchors.fill: parent 413 | anchors.leftMargin: 16 414 | anchors.rightMargin: 16 415 | spacing: 12 416 | 417 | Text { text: "⌕"; color: searchField.activeFocus ? root.cLavender : root.cOverlay0; font.pixelSize: 20; font.bold: true } 418 | 419 | TextField { 420 | id: searchField 421 | Layout.fillWidth: true 422 | Layout.alignment: Qt.AlignVCenter 423 | background: null 424 | color: root.cText 425 | font.pixelSize: 15 426 | selectedTextColor: root.cBase 427 | selectionColor: root.cLavender 428 | placeholderText: 'Search themes...' 429 | placeholderTextColor: root.cOverlay0 430 | focus: true 431 | 432 | onTextChanged: root.updateFilter() 433 | Component.onCompleted: Qt.callLater(function () { forceActiveFocus(); }); 434 | 435 | Keys.onDownPressed: flavorList.currentIndex = Math.min(flavorList.currentIndex + 1, flavorList.count - 1); 436 | Keys.onUpPressed: flavorList.currentIndex = Math.max(flavorList.currentIndex - 1, 0); 437 | Keys.onEnterPressed: triggerSelection() 438 | Keys.onReturnPressed: triggerSelection() 439 | Keys.onEscapePressed: Qt.quit() 440 | 441 | function triggerSelection() { 442 | if (displayModel.count > 0) { 443 | var item = displayModel.get(flavorList.currentIndex); 444 | if (item) root.setFlavour(item.flavourId); 445 | } 446 | } 447 | } 448 | } 449 | } 450 | } 451 | 452 | // --- INFO POPUP OVERLAY --- 453 | Rectangle { 454 | id: infoPopup 455 | anchors.fill: parent 456 | color: Qt.alpha(root.cBase, 0.95) 457 | radius: 20 458 | z: 100 459 | visible: root.showInfoPopup 460 | opacity: visible ? 1 : 0 461 | 462 | Behavior on opacity { NumberAnimation { duration: 200 } } 463 | 464 | MouseArea { 465 | anchors.fill: parent 466 | // Block clicks from going to the layers below 467 | } 468 | 469 | ColumnLayout { 470 | anchors.centerIn: parent 471 | spacing: 20 472 | width: parent.width - 60 473 | 474 | Text { 475 | text: "About QuickSwitch" 476 | color: root.cMauve 477 | font.pixelSize: 24 478 | font.bold: true 479 | Layout.alignment: Qt.AlignHCenter 480 | } 481 | 482 | Rectangle { 483 | Layout.fillWidth: true 484 | Layout.preferredHeight: 1 485 | color: root.cSurface1 486 | } 487 | 488 | Text { 489 | text: "Created by MannuVilasara" 490 | color: root.cText 491 | font.pixelSize: 16 492 | Layout.alignment: Qt.AlignHCenter 493 | } 494 | 495 | Text { 496 | text: "If you like this project, please\nconsider giving it a star on GitHub!" 497 | color: root.cSubtext0 498 | font.pixelSize: 14 499 | horizontalAlignment: Text.AlignHCenter 500 | Layout.alignment: Qt.AlignHCenter 501 | } 502 | 503 | // GitHub Star Button 504 | Rectangle { 505 | Layout.preferredWidth: 200 506 | Layout.preferredHeight: 45 507 | Layout.alignment: Qt.AlignHCenter 508 | radius: 12 509 | color: starBtn.containsMouse ? root.cLavender : root.cSurface0 510 | 511 | RowLayout { 512 | anchors.centerIn: parent 513 | spacing: 10 514 | 515 | Text { 516 | text: "★" 517 | color: starBtn.containsMouse ? root.cBase : root.cYellow 518 | font.pixelSize: 20 519 | } 520 | Text { 521 | text: "Star on GitHub" 522 | color: starBtn.containsMouse ? root.cBase : root.cText 523 | font.bold: true 524 | font.pixelSize: 14 525 | } 526 | } 527 | 528 | MouseArea { 529 | id: starBtn 530 | anchors.fill: parent 531 | hoverEnabled: true 532 | cursorShape: Qt.PointingHandCursor 533 | onClicked: Qt.openUrlExternally("https://github.com/MannuVilasara/qswitch") 534 | } 535 | } 536 | 537 | Item { Layout.preferredHeight: 10 } 538 | 539 | // Close Info Button 540 | Rectangle { 541 | Layout.preferredWidth: 100 542 | Layout.preferredHeight: 36 543 | Layout.alignment: Qt.AlignHCenter 544 | radius: 10 545 | color: closeInfoBtn.containsMouse ? root.cSurface1 : "transparent" 546 | border.color: root.cOverlay0 547 | border.width: 1 548 | 549 | Text { 550 | anchors.centerIn: parent 551 | text: "Close" 552 | color: root.cOverlay0 553 | } 554 | 555 | MouseArea { 556 | id: closeInfoBtn 557 | anchors.fill: parent 558 | hoverEnabled: true 559 | cursorShape: Qt.PointingHandCursor 560 | onClicked: root.showInfoPopup = false 561 | } 562 | } 563 | } 564 | } 565 | } 566 | } 567 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------