├── .gitignore ├── examples ├── screenshot-1.png ├── screenshot-2.png └── README.md ├── quicktoggler@shihira.github.com ├── stylesheet.css ├── entries.json ├── metadata.json ├── Makefile ├── locale │ ├── zh_CN │ │ └── LC_MESSAGES │ │ │ └── gnome-extension-quicktoggler.po │ ├── gnome-extension-quicktoggler.pot │ └── de │ │ └── LC_MESSAGES │ │ └── gnome-extension-quicktoggler.po ├── schemas │ └── org.gnome.shell.extensions.quicktoggler.gschema.xml ├── addentry.sh ├── convenience.js ├── prefs.js ├── core.js ├── extension.js ├── COPYING └── prefs.glade ├── README.md └── icon.svg /.gitignore: -------------------------------------------------------------------------------- 1 | gschemas.compiled 2 | *.mo 3 | *~ 4 | -------------------------------------------------------------------------------- /examples/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shihira/gnome-extension-quicktoggler/HEAD/examples/screenshot-1.png -------------------------------------------------------------------------------- /examples/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shihira/gnome-extension-quicktoggler/HEAD/examples/screenshot-2.png -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/stylesheet.css: -------------------------------------------------------------------------------- 1 | .search-box { 2 | padding-left: 10px; 3 | padding-right: 10px; 4 | padding-top: 0px; 5 | padding-bottom: 10px; 6 | 7 | min-width: 200px; 8 | min-height: 30px; 9 | } 10 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/entries.json: -------------------------------------------------------------------------------- 1 | { 2 | "entries": [ 3 | { 4 | "type": "launcher", 5 | "title": "Add New Entry", 6 | "command": "~/.local/share/gnome-shell/extensions/quicktoggler@shihira.github.com/addentry.sh" 7 | }, 8 | { 9 | "type": "launcher", 10 | "title": "Edit Entries", 11 | "command": "gnome-open ~/.entries.json" 12 | }, 13 | { 14 | "type": "launcher", 15 | "title": "Nautilus", 16 | "command": "nautilus" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Quick Toggler", 3 | "shell-version": ["3.16", "3.18", "3.20", "3.22", "3.24", "3.26"], 4 | "description": "A high customizable extension that enables you to toggle, control services and launch apps in one click. For details, please view the github homepage.", 5 | "uuid": "quicktoggler@shihira.github.com", 6 | "url": "https://github.com/Shihira/gnome-extension-quicktoggler/", 7 | "gettext-domain": "gnome-extension-quicktoggler", 8 | "settings-schema": "org.gnome.shell.extensions.quicktoggler" 9 | } 10 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/Makefile: -------------------------------------------------------------------------------- 1 | NAME=gnome-extension-quicktoggler 2 | PO=$(wildcard locale/**/LC_MESSAGES/$(NAME).po) 3 | MO=$(PO:.po=.mo) 4 | 5 | .PHONY: all schemas locale update_po install 6 | 7 | all: schemas locale 8 | 9 | schemas: schemas/gschemas.compiled 10 | 11 | locale: $(MO) 12 | 13 | uninstall: 14 | rm -rv ~/.local/share/gnome-shell/extensions/quicktoggler@shihira.github.com || echo 15 | 16 | install: uninstall all 17 | cp -Rv . ~/.local/share/gnome-shell/extensions/quicktoggler@shihira.github.com 18 | 19 | schemas/gschemas.compiled: schemas/org.gnome.shell.extensions.quicktoggler.gschema.xml 20 | glib-compile-schemas schemas 21 | 22 | update_po: locale/$(NAME).pot 23 | find . -name \*.po -exec msgmerge -U {} locale/gnome-extension-quicktoggler.pot \; 24 | 25 | locale/%/LC_MESSAGES/$(NAME).mo: locale/%/LC_MESSAGES/$(NAME).po 26 | msgfmt $^ -o `dirname $^`/gnome-extension-quicktoggler.mo 27 | 28 | locale/$(NAME).pot: prefs.glade extension.js 29 | xgettext prefs.glade extension.js -o locale/$(NAME).pot 30 | 31 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/locale/zh_CN/LC_MESSAGES/gnome-extension-quicktoggler.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "" 3 | "Report-Msgid-Bugs-To: \n" 4 | "POT-Creation-Date: 2017-04-18 08:48+0800\n" 5 | "Last-Translator: Shihira Fung \n" 6 | "Language: zh_CN\n" 7 | "MIME-Version: 1.0\n" 8 | "Content-Type: text/plain; charset=utf-8\n" 9 | "Content-Transfer-Encoding: 8bit\n" 10 | 11 | #: prefs.glade:33 12 | msgid " Custom Entries File" 13 | msgstr "自定义Entries文件" 14 | 15 | #: prefs.glade:78 16 | msgid "Indicator Icon" 17 | msgstr "指示器图标" 18 | 19 | #: prefs.glade:115 20 | #, fuzzy 21 | msgid "Indicator Text" 22 | msgstr "指示器文本" 23 | 24 | #: prefs.glade:158 25 | msgid "Shortcut" 26 | msgstr "快捷键" 27 | 28 | #: prefs.glade:171 29 | msgid "Press to Edit" 30 | msgstr "点击右边按钮编辑" 31 | 32 | #: prefs.glade:191 33 | msgid "New Accerlator..." 34 | msgstr "新快捷键……" 35 | 36 | #: prefs.glade:220 37 | msgid "Detection Interval (ms)" 38 | msgstr "侦测间隔(ms)" 39 | 40 | #: prefs.glade:259 41 | msgid "Show Filter" 42 | msgstr "启用搜索" 43 | 44 | #: prefs.glade:296 45 | msgid "Logging" 46 | msgstr "日志" 47 | 48 | #: prefs.glade:348 49 | msgid "GNOME Default" 50 | msgstr "GNOME默认方式" 51 | 52 | #: prefs.glade:368 53 | msgid "Log to File" 54 | msgstr "记录到文件" 55 | 56 | #: prefs.glade:419 57 | msgid "Notification" 58 | msgstr "通知" 59 | 60 | #: prefs.glade:470 61 | msgid "Notify me when a process exited unexpectedly" 62 | msgstr "进程异常退出时通知我" 63 | 64 | #: prefs.glade:485 65 | msgid "Notify me when the extension failed to work normally" 66 | msgstr "扩展无法正常工作时通知我" 67 | 68 | #: prefs.glade:500 69 | msgid "Notify me when the state of a toggler was changed" 70 | msgstr "Toggler状态变化时通知我" 71 | 72 | #: prefs.glade:534 73 | msgid "Apply" 74 | msgstr "应用" 75 | 76 | #: prefs.glade:547 77 | msgid "Restore" 78 | msgstr "恢复" 79 | 80 | #: extension.js:119 81 | msgid "Filter" 82 | msgstr "搜索" 83 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/locale/gnome-extension-quicktoggler.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2017-04-18 08:48+0800\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: prefs.glade:33 21 | msgid " Custom Entries File" 22 | msgstr "" 23 | 24 | #: prefs.glade:78 25 | msgid "Indicator Icon" 26 | msgstr "" 27 | 28 | #: prefs.glade:115 29 | msgid "Indicator Text" 30 | msgstr "" 31 | 32 | #: prefs.glade:158 33 | msgid "Shortcut" 34 | msgstr "" 35 | 36 | #: prefs.glade:171 37 | msgid "Press to Edit" 38 | msgstr "" 39 | 40 | #: prefs.glade:191 41 | msgid "New Accerlator..." 42 | msgstr "" 43 | 44 | #: prefs.glade:220 45 | msgid "Detection Interval (ms)" 46 | msgstr "" 47 | 48 | #: prefs.glade:259 49 | msgid "Show Filter" 50 | msgstr "" 51 | 52 | #: prefs.glade:296 53 | msgid "Logging" 54 | msgstr "" 55 | 56 | #: prefs.glade:348 57 | msgid "GNOME Default" 58 | msgstr "" 59 | 60 | #: prefs.glade:368 61 | msgid "Log to File" 62 | msgstr "" 63 | 64 | #: prefs.glade:419 65 | msgid "Notification" 66 | msgstr "" 67 | 68 | #: prefs.glade:470 69 | msgid "Notify me when a process exited unexpectedly" 70 | msgstr "" 71 | 72 | #: prefs.glade:485 73 | msgid "Notify me when the extension failed to work normally" 74 | msgstr "" 75 | 76 | #: prefs.glade:500 77 | msgid "Notify me when the state of a toggler was changed" 78 | msgstr "" 79 | 80 | #: prefs.glade:534 81 | msgid "Apply" 82 | msgstr "" 83 | 84 | #: prefs.glade:547 85 | msgid "Restore" 86 | msgstr "" 87 | 88 | #: extension.js:119 89 | msgid "Filter" 90 | msgstr "" 91 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/schemas/org.gnome.shell.extensions.quicktoggler.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | '' 6 | Entries File 7 | Absolute path to your configuration file. Leave empty to use ~/.entries.json. 8 | 9 | 10 | 10000 11 | Detection Interval 12 | Milliseconds of the interval between two detection. 13 | 14 | 15 | '' 16 | Log File 17 | Location (absolute path) of log file, set 'gnome-shell' to use global.log or leave emtpy to disable logging. 18 | 19 | 20 | 'emblem-system-symbolic' 21 | Indicator Icon 22 | Indicator icon. 23 | 24 | 25 | '' 26 | Indicator Text 27 | Indicator text. 28 | 29 | 30 | ['<Super>j'] 31 | Menu Shortcut 32 | Menu shortcut. 33 | 34 | 35 | ['ext'] 36 | Notification Conditions 37 | Array of notification conditions. Please select from 'proc', 'ext', 'state'. 38 | 39 | 40 | true 41 | Whether to show the filter. 42 | Whether to show the filter. 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/locale/de/LC_MESSAGES/gnome-extension-quicktoggler.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2017-04-18 08:48+0800\n" 11 | "PO-Revision-Date: 2017-04-13 15:35+0200\n" 12 | "Last-Translator: Jonatan Hatakeyama Zeidler \n" 13 | "Language-Team: \n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 1.8.7.1\n" 19 | 20 | #: prefs.glade:33 21 | msgid " Custom Entries File" 22 | msgstr "Angepasste Einträgedatei" 23 | 24 | #: prefs.glade:78 25 | msgid "Indicator Icon" 26 | msgstr "Indikatorsymbol" 27 | 28 | #: prefs.glade:115 29 | msgid "Indicator Text" 30 | msgstr "Indikatortext" 31 | 32 | #: prefs.glade:158 33 | msgid "Shortcut" 34 | msgstr "Tastenkürzel" 35 | 36 | #: prefs.glade:171 37 | msgid "Press to Edit" 38 | msgstr "zum Anpassen klicken" 39 | 40 | #: prefs.glade:191 41 | msgid "New Accerlator..." 42 | msgstr "Neuer Beschleuniger …" 43 | 44 | #: prefs.glade:220 45 | msgid "Detection Interval (ms)" 46 | msgstr "Aktualisierungsintervall (ms):" 47 | 48 | #: prefs.glade:259 49 | msgid "Show Filter" 50 | msgstr "Filter anzeigen" 51 | 52 | #: prefs.glade:296 53 | msgid "Logging" 54 | msgstr "Log" 55 | 56 | #: prefs.glade:348 57 | msgid "GNOME Default" 58 | msgstr "GNOME-Standard" 59 | 60 | #: prefs.glade:368 61 | msgid "Log to File" 62 | msgstr "In Logdatei" 63 | 64 | #: prefs.glade:419 65 | msgid "Notification" 66 | msgstr "Benachrichtigungen" 67 | 68 | #: prefs.glade:470 69 | msgid "Notify me when a process exited unexpectedly" 70 | msgstr "Benachrichten, wenn ein Prozess unerwartet beendet wird" 71 | 72 | #: prefs.glade:485 73 | msgid "Notify me when the extension failed to work normally" 74 | msgstr "Benachrichtigen, wenn die Erweiterung nicht normal funktioniert" 75 | 76 | #: prefs.glade:500 77 | msgid "Notify me when the state of a toggler was changed" 78 | msgstr "Benachrichtigen, wenn ein Umschalter den Zustand wechselt" 79 | 80 | #: prefs.glade:534 81 | msgid "Apply" 82 | msgstr "Anwenden" 83 | 84 | #: prefs.glade:547 85 | msgid "Restore" 86 | msgstr "Zurücksetzen" 87 | 88 | #: extension.js:119 89 | msgid "Filter" 90 | msgstr "Filtern" 91 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/addentry.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | entries_json="$1" 4 | [[ "$1" = "" ]] && entries_json=$HOME/.entries.json 5 | [[ -x $(which jq) ]] || zenity --error --text "Please install jq" 6 | 7 | entries_content=$(cat "$entries_json") 8 | cp "$entries_json" "${entries_json}.old" 9 | 10 | type=$(zenity --title "Add New Entry" \ 11 | --forms --text="Entry Properties" \ 12 | --add-combo="Type" --combo-values="launcher|custom|systemd|tmux") || exit 13 | 14 | if [[ "$type" = "launcher" ]]; then 15 | prop=$(zenity --title "Add New Launcher Entry" \ 16 | --forms --text="Entry Properties" \ 17 | --add-entry "Title" --add-entry "Command") || exit 18 | title=$(echo "$prop" | cut -d'|' -f 1) 19 | command=$(echo "$prop" | cut -d'|' -f 2-) 20 | 21 | echo "Adding $title" 22 | 23 | echo "$entries_content" | \ 24 | jq '.entries += [{ "type": "launcher", "title": $title, "command": $command }]' \ 25 | --arg title "$title" \ 26 | --arg command "$command" > "$entries_json" 27 | elif [[ "$type" = "custom" ]]; then 28 | json=$(zenity --title "Add New Custom Entry" \ 29 | --forms --text="Entry Properties" \ 30 | --add-entry "JSON Entry") || exit 31 | echo "$entries_content" | \ 32 | jq '.entries += [$json]' \ 33 | --argjson json "$json" > "$entries_json" 34 | elif [[ "$type" = "systemd" ]]; then 35 | echo "Finding services..." 36 | services=$(systemctl list-unit-files | grep '.*\.service' | cut -d' ' -f 1 | paste -sd '|') 37 | echo "$services" 38 | 39 | prop=$(zenity --title "Add New Systemd Entry" \ 40 | --forms --text="Entry Properties" \ 41 | --add-entry "Title" \ 42 | --add-list="Services" --list-values="$services") || exit 43 | title=$(echo "$prop" | cut -d'|' -f 1) 44 | unit=$(echo "$prop" | cut -d'|' -f 2-) 45 | 46 | echo "Adding $title: $unit" 47 | 48 | echo "$entries_content" | \ 49 | jq '.entries += [{ "type": "systemd", "title": $title, "unit": $unit }]' \ 50 | --arg title "$title" \ 51 | --arg unit "$unit" > "$entries_json" 52 | elif [[ "$type" = "tmux" ]]; then 53 | prop=$(zenity --title "Add New Launcher Entry" \ 54 | --forms --text="Entry Properties" \ 55 | --add-entry "Title" --add-entry "Session Name" --add-entry "Command") || exit 56 | title=$(echo "$prop" | cut -d'|' -f 1) 57 | session=$(echo "$prop" | cut -d'|' -f 2) 58 | command=$(echo "$prop" | cut -d'|' -f 3-) 59 | 60 | echo "Adding $title: $session" 61 | 62 | echo "$entries_content" | \ 63 | jq '.entries += [{ "type": "tmux", "title": $title, "session": $session, "command": $command }]' \ 64 | --arg title "$title" \ 65 | --arg session "$session" \ 66 | --arg command "$command" > "$entries_json" 67 | fi 68 | 69 | if [[ $? = 0 ]]; then 70 | zenity --info --text "Configuration has been successfully updated. Old file has renamed to ${entries_json}.old" 71 | else 72 | zenity --error --text "Failed to update configuration. Old file has renamed to ${entries_json}.old" 73 | fi 74 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Here are some examples. Put them inside `"entries": [...]` to make them work. 4 | Welcome for more third-party examples. File me pull requests if you want to add 5 | yours. 6 | 7 | NOTE: With `addentry.sh`, simply paste any of the following code snippet as a **custom** entry. 8 | 9 | ## Restart the Extension 10 | 11 | Simply disable and re-enable. Useful when your entries are modified. 12 | 13 | ``` 14 | { 15 | "type": "launcher", 16 | "title": "Restart Extension", 17 | "command": "gnome-shell-extension-tool -d quicktoggler@shihira.github.com && gnome-shell-extension-tool -e quicktoggler@shihira.github.com" 18 | } 19 | ``` 20 | 21 | ## Open Preference Window 22 | 23 | ``` 24 | { 25 | "type": "launcher", 26 | "title": "Edit Preference", 27 | "command": "gnome-shell-extension-prefs quicktoggler@shihira.github.com" 28 | } 29 | ``` 30 | 31 | ## Edit Entries 32 | 33 | Replace the path to your entries.json location. 34 | 35 | ``` 36 | { 37 | "type": "launcher", 38 | "title": "Edit Entries", 39 | "command": "gnome-open ~/.entries.json" 40 | } 41 | ``` 42 | 43 | ## Enable Wifi Hotspot 44 | 45 | Make sure your Hotspot id is `Hotspot` :) 46 | 47 | ``` 48 | { 49 | "type": "toggler", 50 | "title": "Wifi Hotspot", 51 | "command_on": "nmcli con up id Hotspot", 52 | "command_off": "nmcli con down id Hotspot", 53 | "detector": "nmcli con show --active | grep Hotspot" 54 | } 55 | ``` 56 | 57 | ## Run Privileged Command 58 | 59 | Don't forget `env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY` to run GUI apps. 60 | 61 | ``` 62 | { 63 | "type": "launcher", 64 | "title": "Firewall", 65 | "command": "pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY firewall-config" 66 | } 67 | ``` 68 | 69 | ## Toggle Input Method 70 | 71 | Considerable tasks can be done by gsettings. 72 | 73 | ``` 74 | { 75 | "type": "toggler", 76 | "title": "Japanese Input", 77 | "detector": "gsettings get org.gnome.desktop.input-sources sources | grep kkc", 78 | "command_on": "gsettings set org.gnome.desktop.input-sources sources \"[('xkb', 'us'),('ibus', 'kkc')]\"", 79 | "command_off": "gsettings set org.gnome.desktop.input-sources sources \"[('xkb', 'us')]\"" 80 | } 81 | ``` 82 | 83 | ## Ban Suspension on AC 84 | 85 | ``` 86 | { 87 | "type": "toggler", 88 | "title": "No Suspend on AC", 89 | "detector": "gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type | grep nothing", 90 | "command_on": "gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type nothing", 91 | "command_off": "gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type suspend" 92 | } 93 | ``` 94 | 95 | ## Keep Eye on CPU Temperature 96 | 97 | You can print information you care about on the top-bar, especially real-time information, like CPU temperature, stock prices, etc. 98 | 99 | ``` 100 | { 101 | "type": "tmux", 102 | "title": "CPU Temperature", 103 | "session": "cpu-temp", 104 | "command": "while true; do gsettings --schemadir ~/.local/share/gnome-shell/extensions/quicktoggler@shihira.github.com/schemas set org.gnome.shell.extensions.quicktoggler indicator-text \"$(expr $(cat /sys/class/thermal/thermal_zone0/temp) / 1000) deg\"; sleep 2; done" 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/convenience.js: -------------------------------------------------------------------------------- 1 | /* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | /* 3 | Copyright (c) 2011-2012, Giovanni Campagna 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the GNOME nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | const Gettext = imports.gettext; 29 | const Gio = imports.gi.Gio; 30 | 31 | const Config = imports.misc.config; 32 | const ExtensionUtils = imports.misc.extensionUtils; 33 | 34 | /** 35 | * initTranslations: 36 | * @domain: (optional): the gettext domain to use 37 | * 38 | * Initialize Gettext to load translations from extensionsdir/locale. 39 | * If @domain is not provided, it will be taken from metadata['gettext-domain'] 40 | */ 41 | function initTranslations(domain) { 42 | let extension = ExtensionUtils.getCurrentExtension(); 43 | 44 | domain = domain || extension.metadata['gettext-domain']; 45 | 46 | // check if this extension was built with "make zip-file", and thus 47 | // has the locale files in a subfolder 48 | // otherwise assume that extension has been installed in the 49 | // same prefix as gnome-shell 50 | let localeDir = extension.dir.get_child('locale'); 51 | if (localeDir.query_exists(null)) 52 | Gettext.bindtextdomain(domain, localeDir.get_path()); 53 | else 54 | Gettext.bindtextdomain(domain, Config.LOCALEDIR); 55 | } 56 | 57 | /** 58 | * getSettings: 59 | * @schema: (optional): the GSettings schema id 60 | * 61 | * Builds and return a GSettings schema for @schema, using schema files 62 | * in extensionsdir/schemas. If @schema is not provided, it is taken from 63 | * metadata['settings-schema']. 64 | */ 65 | function getSettings(schema) { 66 | let extension = ExtensionUtils.getCurrentExtension(); 67 | 68 | schema = schema || extension.metadata['settings-schema']; 69 | 70 | const GioSSS = Gio.SettingsSchemaSource; 71 | 72 | // check if this extension was built with "make zip-file", and thus 73 | // has the schema files in a subfolder 74 | // otherwise assume that extension has been installed in the 75 | // same prefix as gnome-shell (and therefore schemas are available 76 | // in the standard folders) 77 | let schemaDir = extension.dir.get_child('schemas'); 78 | let schemaSource; 79 | if (schemaDir.query_exists(null)) 80 | schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), 81 | GioSSS.get_default(), 82 | false); 83 | else 84 | schemaSource = GioSSS.get_default(); 85 | 86 | let schemaObj = schemaSource.lookup(schema, true); 87 | if (!schemaObj) 88 | throw new Error('Schema ' + schema + ' could not be found for extension ' 89 | + extension.metadata.uuid + '. Please check your installation.'); 90 | 91 | return new Gio.Settings({ settings_schema: schemaObj }); 92 | } 93 | 94 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/prefs.js: -------------------------------------------------------------------------------- 1 | const Gtk = imports.gi.Gtk; 2 | const GObject = imports.gi.GObject; 3 | const Lang = imports.lang; 4 | 5 | const Me = imports.misc.extensionUtils.getCurrentExtension(); 6 | const Convenience = Me.imports.convenience; 7 | 8 | /* 9 | * How to add an option: 10 | * 11 | * 1. Add the name of widgets in loadNames, it turns abc-efg to abc_efg. 12 | * 2. Use bindSchema to enable auto load settings from dconf 13 | * 3. Specify a getter and a setter, which access the widgets. 14 | */ 15 | 16 | const ENTRIES_FILE = "entries-file"; 17 | const DETECTION_INTERVAL = "detection-interval"; 18 | const LOG_FILE = "log-file"; 19 | const INDICATOR_ICON = "indicator-icon"; 20 | const INDICATOR_TEXT = "indicator-text"; 21 | const MENU_SHORTCUT = "menu-shortcut"; 22 | const NOTIFICATION_COND = "notification-cond"; 23 | const SHOW_FILTER = "show-filter"; 24 | 25 | const PrefsWindow = new Lang.Class({ 26 | Name: "PrefsWindow", 27 | 28 | _init: function(prop) { 29 | this._builder = new Gtk.Builder(); 30 | this._builder.add_from_file(Me.path + "/prefs.glade"); 31 | this.loadNames([ 32 | "layout-prefs", 33 | "check-entries", 34 | "file-entries", 35 | "entry-indicator", 36 | "entry-indicator-text", 37 | "btn-shortcut", 38 | "spin-interval", 39 | "switch-log", 40 | "switch-show-filter", 41 | "radio-log-gnome", 42 | "radio-log-file", 43 | "file-log-file", 44 | "switch-notify", 45 | "check-notify-proc", 46 | "check-notify-ext", 47 | "check-notify-state", 48 | "btn-apply", 49 | "btn-restore", 50 | ]); 51 | 52 | this.bindState(this.switch_log, [ 53 | this.radio_log_gnome, 54 | this.radio_log_file, 55 | this.file_log_file, 56 | ], "state-set"); 57 | 58 | this.bindState(this.radio_log_file, 59 | [this.file_log_file], 60 | "toggled"); 61 | 62 | this.bindState(this.check_entries, 63 | [this.file_entries], 64 | "toggled"); 65 | 66 | this.bindState(this.switch_notify, [ 67 | this.check_notify_proc, 68 | this.check_notify_ext, 69 | this.check_notify_state, 70 | ], "state_set"); 71 | 72 | this.bindSchema(ENTRIES_FILE, "string"); 73 | this.bindSchema(INDICATOR_ICON, "string"); 74 | this.bindSchema(INDICATOR_TEXT, "string"); 75 | this.bindSchema(MENU_SHORTCUT, "strv"); 76 | this.bindSchema(DETECTION_INTERVAL, "int"); 77 | this.bindSchema(LOG_FILE, "string"); 78 | this.bindSchema(NOTIFICATION_COND, "strv"); 79 | this.bindSchema(SHOW_FILTER, "boolean"); 80 | 81 | this.setupSettings(); 82 | this.setupState(); 83 | 84 | this.btn_apply.connect("clicked", 85 | Lang.bind(this, this.storeSettings)); 86 | this.btn_restore.connect("clicked", 87 | Lang.bind(this, this.setupSettings)); 88 | 89 | this.btn_shortcut.connect("key-press-event", 90 | Lang.bind(this, this.shortcutPress)); 91 | this.btn_shortcut.connect("key-release-event", 92 | Lang.bind(this, this.shortcutRelease)); 93 | }, 94 | 95 | //////////////////////////////////////////////////////////////////////////// 96 | // GUI Part 97 | 98 | shortcutPress: function(w, ev) { 99 | if(!w.active) return false; 100 | 101 | let keyval = ev.get_keyval()[1]; 102 | let state = ev.get_state()[1]; 103 | state &= Gtk.accelerator_get_default_mod_mask(); 104 | w.label = String(Gtk.accelerator_name(keyval, state)); 105 | 106 | delete this.prev_shortcut; 107 | 108 | return true; 109 | }, 110 | 111 | shortcutRelease: function(w, ev) { 112 | w.active = false; 113 | return true; 114 | }, 115 | 116 | loadNames: function(names) { 117 | for(let i in names) { 118 | let regname = names[i].replace(/-/g, '_'); 119 | this[regname] = this._builder.get_object(names[i]); 120 | } 121 | }, 122 | 123 | bindState: function(sw, widgets, sig) { 124 | if(!this.state_link) 125 | this.state_link = [] 126 | 127 | function state_handler(_) { 128 | for(let i in widgets) 129 | widgets[i].sensitive = sw.active; 130 | } 131 | 132 | this.state_link.push([sw, state_handler]) 133 | 134 | sw.connect(sig, state_handler); 135 | }, 136 | 137 | setupState: function() { 138 | for(let i in this.state_link) { 139 | this.state_link[i][1](); 140 | } 141 | }, 142 | 143 | //////////////////////////////////////////////////////////////////////////// 144 | // Data Exchange Part 145 | 146 | bindSchema: function(schema_name, type, prop) { 147 | prop = prop || schema_name.replace(/-/, '_'); 148 | if(!this.schema_prop_map) 149 | this.schema_prop_map = {}; 150 | this.schema_prop_map[schema_name] = [type, prop]; 151 | }, 152 | 153 | setupSettings: function() { 154 | for(let sch in this.schema_prop_map) { 155 | let prop = this.schema_prop_map[sch]; 156 | let value = Convenience.getSettings()["get_" + prop[0]](sch); 157 | this[prop[1]] = value; 158 | } 159 | }, 160 | 161 | storeSettings: function() { 162 | for(let sch in this.schema_prop_map) { 163 | let prop = this.schema_prop_map[sch]; 164 | let value = this[prop[1]]; 165 | Convenience.getSettings()["set_" + prop[0]](sch, value); 166 | } 167 | }, 168 | 169 | get entries_file() { 170 | return this.check_entries.active ? 171 | this.file_entries.get_filename() : ""; 172 | }, 173 | set entries_file(f) { 174 | this.check_entries.active = Boolean(f); 175 | if(f) this.file_entries.set_filename(f); 176 | }, 177 | get indicator_icon() { return this.entry_indicator.get_text(); }, 178 | set indicator_icon(t) { this.entry_indicator.set_text(t); }, 179 | get indicator_text() { return this.entry_indicator_text.get_text(); }, 180 | set indicator_text(t) { this.entry_indicator_text.set_text(t); }, 181 | get menu_shortcut() { return [this.btn_shortcut.get_label()]; }, 182 | set menu_shortcut(v) { return this.btn_shortcut.set_label(v[0]); }, 183 | get detection_interval() { return this.spin_interval.value; }, 184 | set detection_interval(i) { this.spin_interval.value = i; }, 185 | get log_file() { 186 | return !this.switch_log.active ? "" : 187 | this.radio_log_gnome.active ? "gnome-shell" : 188 | this.file_log_file.get_filename(); 189 | }, 190 | set log_file(t) { 191 | this.switch_log.active = (t != ""); 192 | if(t == "gnome-shell") 193 | this.radio_log_gnome.active = true; 194 | else 195 | this.file_log_file.set_filename(t); 196 | }, 197 | get notification_cond() { 198 | let arr = []; 199 | if(!this.switch_notify.active) return arr; 200 | 201 | if(this.check_notify_proc.active) arr.push("proc"); 202 | if(this.check_notify_ext.active) arr.push("ext"); 203 | if(this.check_notify_state.active) arr.push("state"); 204 | return arr; 205 | }, 206 | set notification_cond(arr) { 207 | this.switch_notify.active = arr.length != 0; 208 | 209 | this.check_notify_proc.active = arr.indexOf("proc") >= 0; 210 | this.check_notify_ext.active = arr.indexOf("ext") >= 0; 211 | this.check_notify_state.active = arr.indexOf("state") >= 0; 212 | }, 213 | get show_filter() { 214 | return this.switch_show_filter.active; 215 | }, 216 | set show_filter(t) { 217 | this.switch_show_filter.active = t; 218 | }, 219 | }); 220 | 221 | function init() { 222 | Convenience.initTranslations(); 223 | } 224 | 225 | function buildPrefsWidget() { 226 | let w = new PrefsWindow(); 227 | w.layout_prefs.show_all(); 228 | 229 | return w.layout_prefs; 230 | } 231 | 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick Toggler 2 | 3 | Quick Toggler is a GNOME extension providing a handy toggler and command 4 | launcher. All behaviours is controlled by command and their output. 5 | 6 | ## Installation 7 | 8 | The best way would be go to for auto download and installation. 9 | 10 | You can also build from source by: 11 | 12 | ``` 13 | cd quicktoggler@shihira.github.com 14 | make install 15 | ``` 16 | 17 | And restart GNOME. If the extension is not running, you can now enable it in 18 | `gnome-tweak-tool`. 19 | 20 | ## Quick Start 21 | 22 | Create or modify `~/.entries.json` as follows and restart the extension. 23 | 24 | ``` 25 | { 26 | "entries": [ 27 | { 28 | "type": "launcher", 29 | "title": "Nautilus", 30 | "command": "nautilus" 31 | }, 32 | { 33 | "type": "toggler", 34 | "title": "Wifi Hotspot", 35 | "command_on": "nmcli con up id Hotspot", 36 | "command_off": "nmcli con down id Hotspot", 37 | "detector": "nmcli con show --active | grep Hotspot" 38 | }, 39 | { 40 | "type": "systemd", 41 | "title": "Apache Httpd", 42 | "unit": "httpd" 43 | }, 44 | { 45 | "type": "submenu", 46 | "title": "SubMenu", 47 | "entries": [ 48 | { 49 | "type": "tmux", 50 | "title": "Minecraft Server", 51 | "session": "cauldron", 52 | "command": "cd ~/Cauldron; ./start.sh" 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ``` 59 | 60 | Then what you will see is below. For more examples, view 61 | `/examples/README.md`. We provide a tool `addentry.sh` to add new entries to 62 | `~/.entries.json`, which is based on `jq` and `zenity` so please ensure both 63 | to have been installed before using it. 64 | 65 | ![screenshot-1](https://raw.githubusercontent.com/Shihira/gnome-extension-quicktoggler/master/examples/screenshot-1.png) 66 | 67 | You can now even apply a fuzzy filter to your entries. In the screenshot below 68 | 'h' matches words 'Httpd' and 'Hotspot'. 69 | 70 | ![screenshot-2](https://raw.githubusercontent.com/Shihira/gnome-extension-quicktoggler/master/examples/screenshot-2.png) 71 | 72 | ## Configuration 73 | 74 | ### Tweak Tool 75 | 76 | You can customize some items in gnome-tweak-tool. Switch off and then switch on 77 | again the extension after modifying settings. 78 | 79 | ### entries.json 80 | 81 | Defaultly, the extension creates and uses `~/.entries.json` that's under your 82 | home path. Also, you can customize this path in the preference window. When the 83 | path you set points to an inexistent file, the extension will create and 84 | initialize it automatically. 85 | 86 | It should be easy to understand the quick start example. Above you can see 87 | all "entries" are presented in the list `"entries"`. You can consider each of 88 | these entries as an menu item in the extension's pop-up menu. 89 | 90 | Currently five types of entries are supported, three of which are basic and two 91 | are derived (the principle of deriviation will be explained later). 92 | For each entry, common properties are: 93 | 94 | - `type` is always required. You should set it to one of the titles below. 95 | - `title` labels the entry. By default, it is `""` 96 | 97 | Thus they are not listed below. For derived entries, they share the properties 98 | of their base, so namely you can use `auto_on` and `notify_when` in `systemd` 99 | and `tmux` entries. 100 | 101 | #### 1. `launcher` 102 | 103 | Clicking on a launcher entry will simply execute a command. 104 | 105 | | property | default value | comment | 106 | |----------|---------------|---------| 107 | | `command` | `""` | Command to execute on clicked. | 108 | 109 | #### 2. `toggler` 110 | 111 | Toggler entry shows a switch. You can customize the behaviour of turn on and 112 | turn off respectively. 113 | 114 | | property | default value | comment | 115 | |----------|---------------|---------| 116 | | `command_on` | `""` | Command to execute when turning on the switch. | 117 | | `command_off` | `""` | Command to execute when turning off the switch. | 118 | | `detector` | `""` | Detector command. Leave blank to disable detection. | 119 | | `auto_on` | false | Try to keep the switch on unless you turn it off manually. | 120 | | `notify_when` | `[]` | When to send a notification. | 121 | 122 | In `notify_when` you should fill string "on" or "off", which indicates the 123 | extension will notify you when the toggler becomes on or off _unexpectedly_. Use 124 | `["on", "off"]` for both scenarios. For global behaviours, you can set them 125 | in the preference window. 126 | 127 | > __NOTE: HOW DO DETECTORS WORK__ 128 | > 129 | > The extension will run the detector periodically (10 seconds or so), and fetch 130 | > data from its stdout pipe. If the output consists of whitespaces or is empty, 131 | > the detection result is `false`. Otherwise it is `true`. The switch will then 132 | > be switch on or off automatically. 133 | 134 | #### 3. `submenu` 135 | 136 | As is shown in the screenshot above, it shows a sub-menu. 137 | 138 | | property | default value | comment | 139 | |----------|---------------|---------| 140 | | `entries` | REQUIRED | Array of entries in this sub-menu | 141 | 142 | #### 4. `tmux` (derived from toggler) 143 | 144 | When you what to run a program as a daemon which is not natively provided, it is 145 | a good idea to run it in a tmux session. 146 | 147 | | property | default value | comment | 148 | |----------|---------------|---------| 149 | | `session` | REQUIRED | Tmux session name. | 150 | | `command` | `""` | Command to execute in a tmux session. | 151 | 152 | #### 5. `systemd` (derived from toggler) 153 | 154 | Start/stop a systemd unit like httpd, firewalld or something like that. Most 155 | system services provide a systemd way to operate. You will be requested for 156 | password by `pkexec`. 157 | 158 | | property | default value | comment | 159 | |----------|---------------|---------| 160 | | `unit` | REQUIRED | Systemd unit. | 161 | 162 | #### 6. `separator` 163 | 164 | No extra properties. Just a separator. 165 | 166 | ## Derivation 167 | 168 | You can now add your customized entry type through altering the configuration 169 | like this: 170 | 171 | ``` 172 | { 173 | "deftype": { 174 | "opendir": { 175 | "base": "launcher", 176 | "vars": ["path"], 177 | "command": "nautilus ${path}" 178 | }, 179 | "user_systemd": { 180 | "base": "toggler", 181 | "vars": ["unit"], 182 | "command_on": "systemctl start --user ${unit}", 183 | "command_off": "systemctl stop --user ${unit}", 184 | "detector": "systemctl status --user ${unit} | grep \\\\bactiv" 185 | } 186 | }, 187 | "entries": [ 188 | //... 189 | { 190 | "title": "Open Home", 191 | "type": "opendir", 192 | "path": "/home/shihira" 193 | }, 194 | { 195 | "title": "Open Web", 196 | "type": "opendir", 197 | "path": "/var/www" 198 | }, 199 | { 200 | "title": "RedShift", 201 | "type": "user_systemd", 202 | "unit": "redshift" 203 | } 204 | //... 205 | ] 206 | } 207 | ``` 208 | 209 | When an entry applies an user-defined derived type, the extension replaces 210 | corresponding properties with generic ones defined by user, and passes 211 | properties defined in actual entry instances as environment variables. Take 212 | `"opendir"` type as example. When you click on "Open Web" the menu item, 213 | the command being actually executed is equivalent to: 214 | 215 | ``` 216 | path=/var/www bash -c 'nautilus ${path}' 217 | ``` 218 | 219 | The two derived entries currently provided as built-in type are defined in a 220 | way equivalent to this form (Please refer to `core.js`): 221 | 222 | ``` 223 | "systemd": { 224 | base: 'toggler', 225 | vars: ['unit'], 226 | command_on: "pkexec systemctl start ${unit}", 227 | command_off: "pkexec systemctl stop ${unit}", 228 | detector: "systemctl status ${unit} | grep Active:\\\\s\\*activ[ei]", 229 | }, 230 | "tmux": { 231 | base: 'toggler', 232 | vars: ['command', 'session'], 233 | command_on: 'tmux new -d -s ${session} bash -c "${command}"', 234 | command_off: 'tmux kill-session -t ${session}', 235 | detector: 'tmux ls | grep "${session}"', 236 | } 237 | ``` 238 | 239 | NOTE: Only single-level derivation is supported currently. But to be frank, 240 | higher level derivation is actually completely useless, because you cannot use 241 | environment variables in plain text properties (like title). 242 | 243 | ## Footnote 244 | 245 | The extension is still buggy and is tested only on Fedora 24 + GNOME 3.20 (but I 246 | believe it runs on 3.16 and 3.18). If you found any bugs, please file me reports 247 | and paste relavant log in `journalctl -f /usr/bin/gnome-shell` or your custom 248 | log file. 249 | 250 | **Translation is welcome! Currently only English and Chinese are supported. Feel 251 | free to fork and help me in translation.** 252 | 253 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 26 | 29 | 33 | 37 | 38 | 41 | 45 | 49 | 50 | 53 | 57 | 61 | 62 | 71 | 80 | 89 | 93 | 98 | 99 | 103 | 108 | 109 | 113 | 118 | 124 | 129 | 134 | 140 | 141 | 145 | 150 | 156 | 161 | 166 | 172 | 173 | 174 | 193 | 195 | 196 | 198 | image/svg+xml 199 | 201 | 202 | 203 | 204 | 205 | 211 | 220 | 226 | 232 | 238 | 244 | 250 | 256 | 265 | 272 | 273 | 274 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/core.js: -------------------------------------------------------------------------------- 1 | const Gio = imports.gi.Gio; 2 | const GLib = imports.gi.GLib; 3 | const Json = imports.gi.Json; 4 | const Lang = imports.lang; 5 | const Main = imports.ui.main; 6 | 7 | const PopupMenu = imports.ui.popupMenu; 8 | 9 | const Me = imports.misc.extensionUtils.getCurrentExtension(); 10 | const getLogger = Me.imports.extension.getLogger; 11 | 12 | const Entry = new Lang.Class({ 13 | Name: 'Entry', 14 | Abstract: true, 15 | 16 | _init: function(prop) { 17 | this.type = prop.type; 18 | this.title = prop.title || ""; 19 | 20 | this.__vars = prop.__vars || []; 21 | this.updateEnv(prop); 22 | }, 23 | 24 | setTitle: function(text) { 25 | this.item.label.get_clutter_text().set_text(text); 26 | }, 27 | 28 | updateEnv: function(prop) { 29 | this.__env = {} 30 | if(!this.__vars) return; 31 | 32 | for(let i in this.__vars) { 33 | let v = this.__vars[i]; 34 | this.__env[v] = prop[v] ? String(prop[v]) : ""; 35 | } 36 | }, 37 | 38 | // the pulse function should be read as "a pulse arrives" 39 | pulse: function() { }, 40 | 41 | _try_destroy: function() { 42 | try { 43 | if(this.item && this.item.destroy) 44 | this.item.destroy(); 45 | } catch(e) { /* Ignore all errors during destory*/ } 46 | }, 47 | }); 48 | 49 | const DerivedEntry = new Lang.Class({ 50 | Name: 'DerivedEntry', 51 | 52 | _init: function(prop) { 53 | if(!prop.base) 54 | throw new Error("Base entry not specified in type definition."); 55 | 56 | this.base = prop.base; 57 | this.vars = prop.vars || []; 58 | 59 | delete prop.base; 60 | delete prop.vars; 61 | 62 | this.prop = prop; 63 | }, 64 | 65 | createInstance: function(addit_prop) { 66 | let cls = type_map[this.base]; 67 | if(!cls) throw new Error("Bad base class."); 68 | if(cls.createInstance) throw new Error("Not allowed to derive from dervied types"); 69 | 70 | for(let rp in this.prop) 71 | addit_prop[rp] = this.prop[rp]; 72 | addit_prop.__vars = this.vars; 73 | 74 | let instance = new cls(addit_prop); 75 | 76 | return instance; 77 | }, 78 | }); 79 | 80 | /* 81 | * callback: function (stdout, stderr, exit_status) { } 82 | */ 83 | let __pipeOpenQueue = []; 84 | let __pipeExecTimer = null; 85 | 86 | function pipeOpen(cmdline, env, callback) { 87 | let param = [cmdline, env, callback] 88 | __pipeOpenQueue.push(param); 89 | if(__pipeExecTimer === null) { 90 | __pipeExecTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, 91 | function() { 92 | let param = __pipeOpenQueue.shift(); 93 | if(param === undefined) { 94 | __pipeExecTimer = null; 95 | return false; 96 | } 97 | if(realPipeOpen) realPipeOpen(param[0], param[1], param[2]); 98 | return true; 99 | }); 100 | 101 | } 102 | } 103 | 104 | function realPipeOpen(cmdline, env, callback) { 105 | let user_cb = callback; 106 | let proc; 107 | 108 | function wait_cb(_, _res) { 109 | let stdout_pipe = proc.get_stdout_pipe(); 110 | let stderr_pipe = proc.get_stderr_pipe(); 111 | 112 | let stdout_content; 113 | let stderr_content; 114 | 115 | // Only the first GLib.MAXINT16 characters are fetched for optimization. 116 | stdout_pipe.read_bytes_async(GLib.MAXINT16, 0, null, function(osrc, ores) { 117 | stdout_content = String(stdout_pipe.read_bytes_finish(ores).get_data()); 118 | stdout_pipe.close(null); 119 | 120 | stderr_pipe.read_bytes_async(GLib.MAXINT16, 0, null, function(esrc, eres) { 121 | stderr_content = String(stderr_pipe.read_bytes_finish(eres).get_data()); 122 | stderr_pipe.close(null); 123 | 124 | user_cb(stdout_content, stderr_content, proc.get_exit_status()); 125 | }); 126 | }); 127 | } 128 | 129 | if(user_cb) { 130 | let _pipedLauncher = new Gio.SubprocessLauncher({ 131 | flags: 132 | Gio.SubprocessFlags.STDERR_PIPE | 133 | Gio.SubprocessFlags.STDOUT_PIPE 134 | }); 135 | for(let key in env) { 136 | _pipedLauncher.setenv(key, env[key], true); 137 | } 138 | proc = _pipedLauncher.spawnv(['bash', '-c', cmdline]); 139 | proc.wait_async(null, wait_cb); 140 | } else { 141 | // Detached launcher is used to spawn commands that we are not concerned 142 | // about its result. 143 | let _detacLauncher = new Gio.SubprocessLauncher(); 144 | for(let key in env) { 145 | _detacLauncher.setenv(key, env[key], true); 146 | } 147 | proc = _detacLauncher.spawnv(['bash', '-c', cmdline]); 148 | } 149 | 150 | getLogger().info("Spawned " + cmdline); 151 | 152 | return proc.get_identifier(); 153 | } 154 | 155 | function _generalSpawn(command, env, title) { 156 | title = title || "Process"; 157 | pipeOpen(command, env, function(stdout, stderr, exit_status) { 158 | if(exit_status != 0) { 159 | getLogger().warning(stderr); 160 | getLogger().notify("proc", title + 161 | " exited with status " + exit_status, stderr); 162 | } 163 | }); 164 | } 165 | 166 | function quoteShellArg(arg) { 167 | arg = arg.replace(/'/g, "'\"'\"'"); 168 | return "'" + arg + "'"; 169 | } 170 | 171 | // This cache is used to reduce detector cost. Each time creating an item, it 172 | // check if the result of this detector is cached, which prevent the togglers 173 | // from running detector on each creation. This is useful especially in search 174 | // mode. 175 | let _toggler_state_cache = { }; 176 | 177 | const TogglerEntry = new Lang.Class({ 178 | Name: 'TogglerEntry', 179 | Extends: Entry, 180 | 181 | _init: function(prop) { 182 | this.parent(prop); 183 | 184 | this.command_on = prop.command_on || ""; 185 | this.command_off = prop.command_off || ""; 186 | this.detector = prop.detector || ""; 187 | this.auto_on = prop.auto_on || false; 188 | this.notify_when = prop.notify_when || []; 189 | // if the switch is manually turned off, auto_on is disabled. 190 | this._manually_switched_off = false; 191 | }, 192 | 193 | createItem: function() { 194 | this._try_destroy(); 195 | 196 | this.item = new PopupMenu.PopupSwitchMenuItem(this.title, false); 197 | this.item.label.get_clutter_text().set_use_markup(true); 198 | this.item.connect('toggled', Lang.bind(this, this._onManuallyToggled)); 199 | 200 | this._loadState(); 201 | 202 | return this.item; 203 | }, 204 | 205 | _onManuallyToggled: function(_, state) { 206 | // when switched on again, this flag will get cleared. 207 | this._manually_switched_off = !state; 208 | this._storeState(state); 209 | this._onToggled(state); 210 | }, 211 | 212 | _onToggled: function(state) { 213 | if(state) 214 | _generalSpawn(this.command_on, this.__env, this.title); 215 | else 216 | _generalSpawn(this.command_off, this.__env, this.title); 217 | }, 218 | 219 | _detect: function(callback) { 220 | // abort detecting if detector is an empty string 221 | if(!this.detector) 222 | return; 223 | 224 | pipeOpen(this.detector, this.__env, function(out) { 225 | out = String(out); 226 | callback(!Boolean(out.match(/^\s*$/))); 227 | }); 228 | }, 229 | 230 | compareState: function(new_state) { 231 | // compare the new state with cached state 232 | // notify when state is different 233 | let old_state = _toggler_state_cache[this.detector]; 234 | if(old_state === undefined) return; 235 | if(old_state == new_state) return; 236 | 237 | if(this.notify_when.indexOf(new_state ? "on" : "off") >= 0) { 238 | let not_str = this.title + (new_state ? " started." : " stopped."); 239 | if(!new_state && this.auto_on) 240 | not_str += " Attempt to restart it now."; 241 | getLogger().notify("state", not_str); 242 | } 243 | }, 244 | 245 | _storeState: function(state) { 246 | let hash = JSON.stringify({ env: this.__env, detector: this.detector }); 247 | _toggler_state_cache[hash] = state; 248 | }, 249 | 250 | _loadState: function() { 251 | let hash = JSON.stringify({ env: this.__env, detector: this.detector }); 252 | let state = _toggler_state_cache[hash]; 253 | if(state !== undefined) 254 | this.item.setToggleState(state); // doesn't emit 'toggled' 255 | }, 256 | 257 | pulse: function() { 258 | this._detect(Lang.bind(this, function(state) { 259 | this.compareState(state); 260 | 261 | this._storeState(state); 262 | this._loadState(); 263 | //global.log(this.title + ': ' + this._manually_switched_off); 264 | 265 | if(!state && !this._manually_switched_off && this.auto_on) 266 | // do not call setToggleState here, because command_on may fail 267 | this._onToggled(this.item, true); 268 | })); 269 | }, 270 | 271 | perform: function() { 272 | this.item.toggle(); 273 | }, 274 | }); 275 | 276 | const LauncherEntry = new Lang.Class({ 277 | Name: 'LauncherEntry', 278 | Extends: Entry, 279 | 280 | _init: function(prop) { 281 | this.parent(prop); 282 | 283 | this.command = prop.command || ""; 284 | }, 285 | 286 | createItem: function() { 287 | this._try_destroy(); 288 | 289 | this.item = new PopupMenu.PopupMenuItem(this.title); 290 | this.item.label.get_clutter_text().set_use_markup(true); 291 | this.item.connect('activate', Lang.bind(this, this._onClicked)); 292 | 293 | return this.item; 294 | }, 295 | 296 | _onClicked: function(_) { 297 | _generalSpawn(this.command, this.__env, this.title); 298 | }, 299 | 300 | perform: function() { 301 | this.item.emit('activate'); 302 | }, 303 | }); 304 | 305 | const SubMenuEntry = new Lang.Class({ 306 | Name: 'SubMenuEntry', 307 | Extends: Entry, 308 | 309 | _init: function(prop) { 310 | this.parent(prop) 311 | 312 | if(prop.entries == undefined) 313 | throw new Error("Expected entries provided in submenu entry."); 314 | 315 | this.entries = []; 316 | 317 | for(let i in prop.entries) { 318 | let entry_prop = prop.entries[i]; 319 | let entry = createEntry(entry_prop); 320 | this.entries.push(entry); 321 | } 322 | }, 323 | 324 | createItem: function() { 325 | this._try_destroy(); 326 | 327 | this.item = new PopupMenu.PopupSubMenuMenuItem(this.title); 328 | this.item.label.get_clutter_text().set_use_markup(true); 329 | for(let i in this.entries) { 330 | let entry = this.entries[i]; 331 | this.item.menu.addMenuItem(entry.createItem()); 332 | } 333 | 334 | return this.item; 335 | }, 336 | 337 | pulse: function() { 338 | for(let i in this.entries) { 339 | let entry = this.entries[i]; 340 | entry.pulse(); 341 | } 342 | } 343 | }); 344 | 345 | const SeparatorEntry = new Lang.Class({ 346 | Name: 'SeparatorEntry', 347 | Extends: Entry, 348 | 349 | _init: function(prop) { }, 350 | 351 | createItem: function() { 352 | this._try_destroy(); 353 | 354 | this.item = new PopupMenu.PopupSeparatorMenuItem(this.title); 355 | this.item.label.get_clutter_text().set_use_markup(true); 356 | 357 | return this.item; 358 | }, 359 | }); 360 | 361 | let type_map = {}; 362 | 363 | //////////////////////////////////////////////////////////////////////////////// 364 | // Config Loader loads config from JSON file. 365 | 366 | // convert Json Nodes (GLib based) to native javascript value. 367 | function convertJson(node) { 368 | if(node.get_node_type() == Json.NodeType.VALUE) 369 | return node.get_value(); 370 | if(node.get_node_type() == Json.NodeType.OBJECT) { 371 | let obj = {} 372 | node.get_object().foreach_member(function(_, k, v_n) { 373 | obj[k] = convertJson(v_n); 374 | }); 375 | return obj; 376 | } 377 | if(node.get_node_type() == Json.NodeType.ARRAY) { 378 | let arr = [] 379 | node.get_array().foreach_element(function(_, i, elem) { 380 | arr.push(convertJson(elem)); 381 | }); 382 | return arr; 383 | } 384 | return null; 385 | } 386 | 387 | // 388 | function createEntry(entry_prop) { 389 | if(!entry_prop.type) 390 | throw new Error("No type specified in entry."); 391 | 392 | let cls = type_map[entry_prop.type]; 393 | if(!cls) 394 | throw new Error("Incorrect type '" + entry_prop.type + "'"); 395 | else if(cls.createInstance) 396 | return cls.createInstance(entry_prop); 397 | 398 | return new cls(entry_prop); 399 | } 400 | 401 | const ConfigLoader = new Lang.Class({ 402 | Name: 'ConfigLoader', 403 | 404 | _init: function(filename) { 405 | if(filename) 406 | this.loadConfig(filename); 407 | }, 408 | 409 | loadConfig: function(filename) { 410 | // reset type_map everytime load the config 411 | type_map = { 412 | launcher: LauncherEntry, 413 | toggler: TogglerEntry, 414 | submenu: SubMenuEntry, 415 | separator: SeparatorEntry 416 | }; 417 | 418 | type_map.systemd = new DerivedEntry({ 419 | base: 'toggler', 420 | vars: ['unit'], 421 | command_on: "pkexec systemctl start ${unit}", 422 | command_off: "pkexec systemctl stop ${unit}", 423 | detector: "systemctl status ${unit} | grep Active:\\\\s\\*activ[ei]", 424 | }); 425 | 426 | type_map.tmux = new DerivedEntry({ 427 | base: 'toggler', 428 | vars: ['command', 'session'], 429 | command_on: 'tmux new -d -s ${session} bash -c "${command}"', 430 | command_off: 'tmux kill-session -t ${session}', 431 | detector: 'tmux has -t "${session}" 2>/dev/null && echo yes', 432 | }); 433 | 434 | /* 435 | * Refer to README file for detailed config file format. 436 | */ 437 | this.entries = []; // CAUTION: remove all entries. 438 | 439 | let config_parser = new Json.Parser(); 440 | config_parser.load_from_file(filename); 441 | 442 | let conf = convertJson(config_parser.get_root()); 443 | if(conf.entries == undefined) 444 | throw new Error("Key 'entries' not found."); 445 | if(conf.deftype) { 446 | for(let tname in conf.deftype) { 447 | if(type_map[tname]) 448 | throw new Error("Type \""+tname+"\" duplicated."); 449 | type_map[tname] = new DerivedEntry(conf.deftype[tname]); 450 | } 451 | } 452 | 453 | for(let conf_i in conf.entries) { 454 | let entry_prop = conf.entries[conf_i]; 455 | this.entries.push(createEntry(entry_prop)); 456 | } 457 | }, 458 | }); 459 | 460 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/extension.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Developer: Shihira Fung 3 | * Date: Apr 27, 2016 4 | * License: GPLv2 5 | */ 6 | 7 | const Main = imports.ui.main; 8 | const GLib = imports.gi.GLib; 9 | const Gio = imports.gi.Gio; 10 | const PopupMenu = imports.ui.popupMenu; 11 | const PanelMenu = imports.ui.panelMenu; 12 | const St = imports.gi.St; 13 | const Clutter = imports.gi.Clutter; 14 | const Meta = imports.gi.Meta; 15 | const Shell = imports.gi.Shell; 16 | const Lang = imports.lang; 17 | 18 | const Me = imports.misc.extensionUtils.getCurrentExtension(); 19 | const Core = Me.imports.core; 20 | const Convenience = Me.imports.convenience; 21 | const Prefs = Me.imports.prefs; 22 | 23 | const Gettext = imports.gettext.domain("gnome-extension-quicktoggler"); 24 | const _ = Gettext.gettext; 25 | 26 | const LOGGER_INFO = 0; 27 | const LOGGER_WARNING = 1; 28 | const LOGGER_ERROR = 2; 29 | 30 | const Logger = new Lang.Class({ 31 | Name: 'Logger', 32 | 33 | _init: function(log_file) { 34 | this._log_file = log_file; 35 | // initailize log_backend 36 | if(!log_file) 37 | this._initEmptyLog(); 38 | else if(log_file == "gnome-shell") 39 | this._initGnomeLog(); 40 | else 41 | this._initFileLog(); 42 | 43 | this.level = LOGGER_WARNING; 44 | 45 | this.info = function(t) { 46 | if(this.level <= LOGGER_INFO) this.log(t) 47 | }; 48 | this.warning = function(t) { 49 | if(this.level <= LOGGER_WARNING) this.log(t) 50 | }; 51 | this.error = function(t) { 52 | if(this.level <= LOGGER_ERROR) this.log(t); 53 | }; 54 | }, 55 | 56 | _initEmptyLog: function() { 57 | this.log = function(_) { }; 58 | }, 59 | 60 | _initGnomeLog: function() { 61 | this.log = function(s) { 62 | global.log("[QuickToggler] " + s); 63 | }; 64 | }, 65 | 66 | _initFileLog: function() { 67 | this.log = function(s) { 68 | // all operations are synchronous: any needs to optimize? 69 | if(!this._output_file || !this._output_file.query_exists(null) || 70 | !this._fstream || this._fstream.is_closed()) { 71 | 72 | this._output_file = Gio.File.new_for_path(this._log_file); 73 | this._fstream = this._output_file.append_to( 74 | Gio.FileCreateFlags.NONE, null); 75 | 76 | if(!this._fstream instanceof Gio.FileIOStream) { 77 | this._initGnomeLog(); 78 | this.log("IOError: Failed to append to " + this._log_file + 79 | " [Gio.IOErrorEnum:" + this._fstream + "]"); 80 | return; 81 | } 82 | } 83 | 84 | this._fstream.write(String(new Date())+" "+s+"\n", null); 85 | this._fstream.flush(null); 86 | } 87 | }, 88 | 89 | notify: function(t, str, details) { 90 | this.ncond = this.ncond || ['proc', 'ext', 'state']; 91 | if(this.ncond.indexOf(t) < 0) return; 92 | Main.notify(str, details || ""); 93 | }, 94 | }); 95 | 96 | let logger = null; 97 | 98 | // lazy-evaluation 99 | function getLogger() { 100 | if(logger === null) 101 | logger = new Logger("gnome-shell"); 102 | return logger; 103 | } 104 | 105 | function errorToString(e) { 106 | if(e instanceof GLib.Error) 107 | return "GLib.Error(" + e.code + ") " + e.message; 108 | if(e instanceof Error) 109 | return e.toString() + "\n" + e.stack; 110 | return e.toString(); 111 | } 112 | 113 | const SearchBox = new Lang.Class({ 114 | Name: 'SearchBox', 115 | 116 | _init: function() { 117 | this.actor = new St.BoxLayout({ style_class: 'search-box' }); 118 | this.search = new St.Entry({ 119 | hint_text: _("Filter"), 120 | x_expand: true, 121 | y_expand: true, 122 | }); 123 | 124 | this.actor.add(this.search); 125 | 126 | this.search.connect('key-release-event', 127 | Lang.bind(this, this._onKeyReleaseEvent)); 128 | }, 129 | 130 | _onKeyReleaseEvent: function(_, ev) { 131 | let text = this.search.get_text().toString(); 132 | let selected = ev.get_key_symbol() == Clutter.KEY_Return; 133 | if(!text) { 134 | // do not perform any searching for empty string. 135 | this._callback()([], selected); 136 | } else { 137 | let ret_ent = this.searchEntries(this.entries, text); 138 | this._callback()(ret_ent, selected); 139 | } 140 | }, 141 | 142 | setSearch: function(entries, callback) { 143 | this._callback = function() { return callback }; 144 | this.entries = entries; 145 | }, 146 | 147 | searchEntries: function(entries, pattern) { 148 | let return_list = []; 149 | for(let e in entries) { 150 | // For entries that have a member `entries`, we enter it(recursion). 151 | // For entries that have a member `perform`, we match it. 152 | let entry = entries[e]; 153 | 154 | if(entry.entries) { 155 | return_list = return_list.concat( 156 | this.searchEntries(entry.entries, pattern)); 157 | continue; 158 | } 159 | 160 | if(entry.perform && this.matchText(entry.title, pattern)) { 161 | return_list.push(entry); 162 | } 163 | } 164 | 165 | return return_list; 166 | }, 167 | 168 | matchText: function(title, pattern) { 169 | // construct regexp pattern: fuzzy search 170 | let regex_sec = []; 171 | regex_sec.push(".*\\b"); 172 | for(let c_i = 0; c_i < pattern.length; ++c_i) { 173 | let c = pattern[c_i]; 174 | let code = "\\x" + c.charCodeAt().toString(16); 175 | let sec = "(.*\\b"+code+"|"+code+")"; 176 | 177 | regex_sec.push(sec); 178 | } 179 | regex_sec.push(".*"); 180 | 181 | let regex = new RegExp(regex_sec.join(""), "i"); 182 | 183 | return regex.test(title); 184 | }, 185 | 186 | reset: function() { 187 | this.search.set_text(""); 188 | }, 189 | }); 190 | 191 | // a global instance of Logger, created when initing indicator 192 | const TogglerIndicator = new Lang.Class({ 193 | Name: 'TogglerIndicator', 194 | Extends: PanelMenu.Button, 195 | 196 | _init: function() { 197 | this.parent(St.Align.START); 198 | this._loadSettings(); 199 | 200 | this.search_mode = false; 201 | }, 202 | 203 | get_layout: function() { 204 | if(!this._layout) { 205 | this._layout = new St.BoxLayout(); 206 | this.actor.add_actor(this._layout); 207 | } 208 | return this._layout; 209 | }, 210 | 211 | _loadSettings: function() { 212 | this._settings = new Convenience.getSettings(); 213 | 214 | this._loadLogger(); // load first 215 | this._loadIcon(); 216 | this._loadText(); 217 | this._loadConfig(); 218 | this._loadSearchBar(); 219 | this._loadPulser(); 220 | this._loadShortcut(); 221 | 222 | this._settings.connect('changed', Lang.bind(this, function(_, key) { 223 | let loaders = {}; 224 | loaders[Prefs.LOG_FILE] = "_loadLogger"; 225 | loaders[Prefs.NOTIFICATION_COND] = "_loadLogger"; 226 | loaders[Prefs.INDICATOR_ICON] = "_loadIcon"; 227 | loaders[Prefs.INDICATOR_TEXT] = "_loadText"; 228 | loaders[Prefs.ENTRIES_FILE] = "_loadConfig"; 229 | loaders[Prefs.DETECTION_INTERVAL] = "_loadPulser"; 230 | 231 | if(loaders[key]) 232 | this[loaders[key]](); 233 | })); 234 | }, 235 | 236 | _loadLogger: function() { 237 | let log_file = this._settings.get_string(Prefs.LOG_FILE); 238 | 239 | logger = new Logger(log_file); 240 | logger.ncond = this._settings.get_strv(Prefs.NOTIFICATION_COND); 241 | }, 242 | 243 | _loadIcon: function() { 244 | let icon_name = this._settings.get_string(Prefs.INDICATOR_ICON); 245 | 246 | if(!this._icon) { 247 | this._icon = new St.Icon({ 248 | icon_name: icon_name, 249 | style_class: 'system-status-icon' 250 | }); 251 | this.get_layout().add_child(this._icon); 252 | } else { 253 | this._icon.set_icon_name(icon_name); 254 | } 255 | }, 256 | 257 | _loadText: function() { 258 | let text = this._settings.get_string(Prefs.INDICATOR_TEXT); 259 | 260 | if(!this._text) { 261 | this._text = new St.Label({ 262 | y_align: Clutter.ActorAlign.CENTER, 263 | }); 264 | 265 | this._text.set_y_expand(true); 266 | this._text.clutter_text.set_use_markup(true); 267 | this.get_layout().add_child(this._text); 268 | } 269 | 270 | if(this._text.clutter_text && this._text.clutter_text.set_markup) 271 | this._text.clutter_text.set_markup(text); 272 | else if(this._text.clutter_text && this._text.clutter_text.set_text) 273 | this._text.clutter_text.set_text(text); 274 | else 275 | getLogger().error("Cannot set indicator string."); 276 | }, 277 | 278 | _loadConfig: function() { 279 | try { 280 | // automatically create configuration file when path is invalid 281 | let entries_file = this._settings.get_string(Prefs.ENTRIES_FILE); 282 | entries_file = entries_file || GLib.get_home_dir() + "/.entries.json"; 283 | 284 | let success = false; 285 | // retry as most 10 times 286 | for(let i = 0; i < 10 && ! success; i++) { 287 | if(!this.entries_file || this.entries_file != entries_file) { 288 | let fileobj = Gio.File.new_for_path(entries_file); 289 | if(!fileobj.query_exists(null)) { 290 | let orgf = Gio.File.new_for_path((Me.path + "/entries.json")); 291 | orgf.copy(fileobj, 0, null, null); 292 | } 293 | 294 | let fileinfo = fileobj.query_info("*", Gio.FileQueryInfoFlags.NONE, null); 295 | if(fileinfo.get_is_symlink()) { 296 | entries_file = fileinfo.get_symlink_target(); 297 | continue; 298 | } 299 | 300 | getLogger().warning("Reloading " + entries_file); 301 | 302 | let monitor = fileobj.monitor(Gio.FileMonitorFlags.NONE, null); 303 | monitor.connect('changed', Lang.bind(this, this._loadConfig)); 304 | this.monitor = monitor; 305 | this.entries_file = entries_file; 306 | 307 | success = true; 308 | } 309 | } 310 | 311 | getLogger().warning("Reloading " + entries_file); 312 | 313 | if(!this._config_loader) 314 | this.config_loader = new Core.ConfigLoader(); 315 | this.config_loader.loadConfig(entries_file); 316 | 317 | this.menu.removeAll(); 318 | 319 | for(let i in this.config_loader.entries) { 320 | let item = this.config_loader.entries[i].createItem(); 321 | this.menu.addMenuItem(item); 322 | } 323 | } catch(e) { 324 | getLogger().error("Error while loading entries:"); 325 | getLogger().error(errorToString(e)); 326 | getLogger().notify("ext", 327 | "An error occurs when loading entries.", 328 | errorToString(e)); 329 | } 330 | }, 331 | 332 | _loadSearchBar: function() { 333 | let is_show_filter = this._settings.get_boolean(Prefs.SHOW_FILTER); 334 | if(!is_show_filter) { 335 | if(this.searchBox) 336 | this.searchBox.destroy(); 337 | this.searchBox = null 338 | return; 339 | } 340 | 341 | if(!this.searchBox) { 342 | this.searchBox = new SearchBox(); 343 | this.menu.box.insert_child_at_index(this.searchBox.actor, 0); 344 | } 345 | this.searchBox.setSearch(this.config_loader.entries, 346 | Lang.bind(this, this._gotSearchResult)); 347 | }, 348 | 349 | _loadPulser: function() { 350 | let interval = this._settings.get_int(Prefs.DETECTION_INTERVAL); 351 | 352 | if(!this._pulser) { 353 | this._pulser = GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, 354 | Lang.bind(this, this.pulse)); 355 | } else { 356 | GLib.Source.remove(this._pulser); 357 | this._pulser = GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, 358 | Lang.bind(this, this.pulse)); 359 | } 360 | 361 | this.pulse(); 362 | }, 363 | 364 | _loadShortcut: function() { 365 | // introduce in different version of GNOME 366 | let kbmode = Shell.ActionMode || Shell.KeyBindingMode || Main.KeybindingMode; 367 | 368 | Main.wm.addKeybinding(Prefs.MENU_SHORTCUT, this._settings, 369 | Meta.KeyBindingFlags.NONE, 370 | kbmode.NORMAL | kbmode.MESSAGE_TRAY, 371 | Lang.bind(this, function() { 372 | this.menu.toggle(); 373 | if(this.searchBox) 374 | this.searchBox.search.grab_key_focus(); 375 | })); 376 | }, 377 | 378 | _onOpenStateChanged: function(menu, open) { 379 | this.parent(menu, open); 380 | 381 | if(open) { 382 | if(this.searchBox) 383 | this.searchBox.reset(); 384 | this._gotSearchResult([], false); 385 | } 386 | }, 387 | 388 | pulse: function() { 389 | try { 390 | for(let i in this.config_loader.entries) { 391 | let conf = this.config_loader.entries[i]; 392 | conf.pulse(); 393 | } 394 | } catch(e) { 395 | getLogger().error("Error during pulse routines (id " + 396 | this._pulser + ")"); 397 | getLogger().error(errorToString(e)); 398 | } 399 | return true; 400 | }, 401 | 402 | _gotSearchResult: function(result, selected) { 403 | // If confirmed, close the menu directly 404 | if(selected) { 405 | if(result[0]) 406 | result[0].perform(); 407 | this.menu.toggle(); 408 | return; 409 | } 410 | 411 | // If result is empty, exit search mode 412 | if(result.length) { 413 | this.search_mode = true; 414 | 415 | this.menu.removeAll(); 416 | for(let i in result) { 417 | let item = result[i].createItem(); 418 | this.menu.addMenuItem(item); 419 | } 420 | } else { 421 | if(!this.search_mode) 422 | return; 423 | 424 | this.search_mode = false; 425 | this.menu.removeAll(); 426 | for(let i in this.config_loader.entries) { 427 | let item = this.config_loader.entries[i].createItem(); 428 | this.menu.addMenuItem(item); 429 | } 430 | } 431 | }, 432 | 433 | destroy: function() { 434 | Main.wm.removeKeybinding(Prefs.MENU_SHORTCUT); 435 | 436 | this.parent(); 437 | }, 438 | }); 439 | 440 | //////////////////////////////////////////////////////////////////////////////// 441 | // Entries 442 | 443 | let indicator; 444 | 445 | function init() { 446 | Convenience.initTranslations("gnome-extension-quicktoggler"); 447 | } 448 | 449 | function enable() { 450 | indicator = new TogglerIndicator(); 451 | Main.panel.addToStatusArea("QuickToggler", indicator); 452 | } 453 | 454 | function disable() { 455 | indicator.destroy(); 456 | } 457 | 458 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 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 licenses for most software are designed to take away your 11 | freedom to share and change it. By contrast, the GNU General Public 12 | License is intended to guarantee your freedom to share and change free 13 | software--to make sure the software is free for all its users. This 14 | General Public License applies to most of the Free Software 15 | Foundation's software and to any other program whose authors commit to 16 | using it. (Some other Free Software Foundation software is covered by 17 | the GNU Library General Public License instead.) You can apply it to 18 | your programs, too. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | this service if you wish), that you receive source code or can get it 24 | if you want it, that you can change the software or use pieces of it 25 | in new free programs; and that you know you can do these things. 26 | 27 | To protect your rights, we need to make restrictions that forbid 28 | anyone to deny you these rights or to ask you to surrender the rights. 29 | These restrictions translate to certain responsibilities for you if you 30 | distribute copies of the software, or if you modify it. 31 | 32 | For example, if you distribute copies of such a program, whether 33 | gratis or for a fee, you must give the recipients all the rights that 34 | you have. You must make sure that they, too, receive or can get the 35 | source code. And you must show them these terms so they know their 36 | rights. 37 | 38 | We protect your rights with two steps: (1) copyright the software, and 39 | (2) offer you this license which gives you legal permission to copy, 40 | distribute and/or modify the software. 41 | 42 | Also, for each author's protection and ours, we want to make certain 43 | that everyone understands that there is no warranty for this free 44 | software. If the software is modified by someone else and passed on, we 45 | want its recipients to know that what they have is not the original, so 46 | that any problems introduced by others will not reflect on the original 47 | authors' reputations. 48 | 49 | Finally, any free program is threatened constantly by software 50 | patents. We wish to avoid the danger that redistributors of a free 51 | program will individually obtain patent licenses, in effect making the 52 | program proprietary. To prevent this, we have made it clear that any 53 | patent must be licensed for everyone's free use or not licensed at all. 54 | 55 | The precise terms and conditions for copying, distribution and 56 | modification follow. 57 | 58 | GNU GENERAL PUBLIC LICENSE 59 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 60 | 61 | 0. This License applies to any program or other work which contains 62 | a notice placed by the copyright holder saying it may be distributed 63 | under the terms of this General Public License. The "Program", below, 64 | refers to any such program or work, and a "work based on the Program" 65 | means either the Program or any derivative work under copyright law: 66 | that is to say, a work containing the Program or a portion of it, 67 | either verbatim or with modifications and/or translated into another 68 | language. (Hereinafter, translation is included without limitation in 69 | the term "modification".) Each licensee is addressed as "you". 70 | 71 | Activities other than copying, distribution and modification are not 72 | covered by this License; they are outside its scope. The act of 73 | running the Program is not restricted, and the output from the Program 74 | is covered only if its contents constitute a work based on the 75 | Program (independent of having been made by running the Program). 76 | Whether that is true depends on what the Program does. 77 | 78 | 1. You may copy and distribute verbatim copies of the Program's 79 | source code as you receive it, in any medium, provided that you 80 | conspicuously and appropriately publish on each copy an appropriate 81 | copyright notice and disclaimer of warranty; keep intact all the 82 | notices that refer to this License and to the absence of any warranty; 83 | and give any other recipients of the Program a copy of this License 84 | along with the Program. 85 | 86 | You may charge a fee for the physical act of transferring a copy, and 87 | you may at your option offer warranty protection in exchange for a fee. 88 | 89 | 2. You may modify your copy or copies of the Program or any portion 90 | of it, thus forming a work based on the Program, and copy and 91 | distribute such modifications or work under the terms of Section 1 92 | above, provided that you also meet all of these conditions: 93 | 94 | a) You must cause the modified files to carry prominent notices 95 | stating that you changed the files and the date of any change. 96 | 97 | b) You must cause any work that you distribute or publish, that in 98 | whole or in part contains or is derived from the Program or any 99 | part thereof, to be licensed as a whole at no charge to all third 100 | parties under the terms of this License. 101 | 102 | c) If the modified program normally reads commands interactively 103 | when run, you must cause it, when started running for such 104 | interactive use in the most ordinary way, to print or display an 105 | announcement including an appropriate copyright notice and a 106 | notice that there is no warranty (or else, saying that you provide 107 | a warranty) and that users may redistribute the program under 108 | these conditions, and telling the user how to view a copy of this 109 | License. (Exception: if the Program itself is interactive but 110 | does not normally print such an announcement, your work based on 111 | the Program is not required to print an announcement.) 112 | 113 | These requirements apply to the modified work as a whole. If 114 | identifiable sections of that work are not derived from the Program, 115 | and can be reasonably considered independent and separate works in 116 | themselves, then this License, and its terms, do not apply to those 117 | sections when you distribute them as separate works. But when you 118 | distribute the same sections as part of a whole which is a work based 119 | on the Program, the distribution of the whole must be on the terms of 120 | this License, whose permissions for other licensees extend to the 121 | entire whole, and thus to each and every part regardless of who wrote it. 122 | 123 | Thus, it is not the intent of this section to claim rights or contest 124 | your rights to work written entirely by you; rather, the intent is to 125 | exercise the right to control the distribution of derivative or 126 | collective works based on the Program. 127 | 128 | In addition, mere aggregation of another work not based on the Program 129 | with the Program (or with a work based on the Program) on a volume of 130 | a storage or distribution medium does not bring the other work under 131 | the scope of this License. 132 | 133 | 3. You may copy and distribute the Program (or a work based on it, 134 | under Section 2) in object code or executable form under the terms of 135 | Sections 1 and 2 above provided that you also do one of the following: 136 | 137 | a) Accompany it with the complete corresponding machine-readable 138 | source code, which must be distributed under the terms of Sections 139 | 1 and 2 above on a medium customarily used for software interchange; or, 140 | 141 | b) Accompany it with a written offer, valid for at least three 142 | years, to give any third party, for a charge no more than your 143 | cost of physically performing source distribution, a complete 144 | machine-readable copy of the corresponding source code, to be 145 | distributed under the terms of Sections 1 and 2 above on a medium 146 | customarily used for software interchange; or, 147 | 148 | c) Accompany it with the information you received as to the offer 149 | to distribute corresponding source code. (This alternative is 150 | allowed only for noncommercial distribution and only if you 151 | received the program in object code or executable form with such 152 | an offer, in accord with Subsection b above.) 153 | 154 | The source code for a work means the preferred form of the work for 155 | making modifications to it. For an executable work, complete source 156 | code means all the source code for all modules it contains, plus any 157 | associated interface definition files, plus the scripts used to 158 | control compilation and installation of the executable. However, as a 159 | special exception, the source code distributed need not include 160 | anything that is normally distributed (in either source or binary 161 | form) with the major components (compiler, kernel, and so on) of the 162 | operating system on which the executable runs, unless that component 163 | itself accompanies the executable. 164 | 165 | If distribution of executable or object code is made by offering 166 | access to copy from a designated place, then offering equivalent 167 | access to copy the source code from the same place counts as 168 | distribution of the source code, even though third parties are not 169 | compelled to copy the source along with the object code. 170 | 171 | 4. You may not copy, modify, sublicense, or distribute the Program 172 | except as expressly provided under this License. Any attempt 173 | otherwise to copy, modify, sublicense or distribute the Program is 174 | void, and will automatically terminate your rights under this License. 175 | However, parties who have received copies, or rights, from you under 176 | this License will not have their licenses terminated so long as such 177 | parties remain in full compliance. 178 | 179 | 5. You are not required to accept this License, since you have not 180 | signed it. However, nothing else grants you permission to modify or 181 | distribute the Program or its derivative works. These actions are 182 | prohibited by law if you do not accept this License. Therefore, by 183 | modifying or distributing the Program (or any work based on the 184 | Program), you indicate your acceptance of this License to do so, and 185 | all its terms and conditions for copying, distributing or modifying 186 | the Program or works based on it. 187 | 188 | 6. Each time you redistribute the Program (or any work based on the 189 | Program), the recipient automatically receives a license from the 190 | original licensor to copy, distribute or modify the Program subject to 191 | these terms and conditions. You may not impose any further 192 | restrictions on the recipients' exercise of the rights granted herein. 193 | You are not responsible for enforcing compliance by third parties to 194 | this License. 195 | 196 | 7. If, as a consequence of a court judgment or allegation of patent 197 | infringement or for any other reason (not limited to patent issues), 198 | conditions are imposed on you (whether by court order, agreement or 199 | otherwise) that contradict the conditions of this License, they do not 200 | excuse you from the conditions of this License. If you cannot 201 | distribute so as to satisfy simultaneously your obligations under this 202 | License and any other pertinent obligations, then as a consequence you 203 | may not distribute the Program at all. For example, if a patent 204 | license would not permit royalty-free redistribution of the Program by 205 | all those who receive copies directly or indirectly through you, then 206 | the only way you could satisfy both it and this License would be to 207 | refrain entirely from distribution of the Program. 208 | 209 | If any portion of this section is held invalid or unenforceable under 210 | any particular circumstance, the balance of the section is intended to 211 | apply and the section as a whole is intended to apply in other 212 | circumstances. 213 | 214 | It is not the purpose of this section to induce you to infringe any 215 | patents or other property right claims or to contest validity of any 216 | such claims; this section has the sole purpose of protecting the 217 | integrity of the free software distribution system, which is 218 | implemented by public license practices. Many people have made 219 | generous contributions to the wide range of software distributed 220 | through that system in reliance on consistent application of that 221 | system; it is up to the author/donor to decide if he or she is willing 222 | to distribute software through any other system and a licensee cannot 223 | impose that choice. 224 | 225 | This section is intended to make thoroughly clear what is believed to 226 | be a consequence of the rest of this License. 227 | 228 | 8. If the distribution and/or use of the Program is restricted in 229 | certain countries either by patents or by copyrighted interfaces, the 230 | original copyright holder who places the Program under this License 231 | may add an explicit geographical distribution limitation excluding 232 | those countries, so that distribution is permitted only in or among 233 | countries not thus excluded. In such case, this License incorporates 234 | the limitation as if written in the body of this License. 235 | 236 | 9. The Free Software Foundation may publish revised and/or new versions 237 | of the General Public License from time to time. Such new versions will 238 | be similar in spirit to the present version, but may differ in detail to 239 | address new problems or concerns. 240 | 241 | Each version is given a distinguishing version number. If the Program 242 | specifies a version number of this License which applies to it and "any 243 | later version", you have the option of following the terms and conditions 244 | either of that version or of any later version published by the Free 245 | Software Foundation. If the Program does not specify a version number of 246 | this License, you may choose any version ever published by the Free Software 247 | Foundation. 248 | 249 | 10. If you wish to incorporate parts of the Program into other free 250 | programs whose distribution conditions are different, write to the author 251 | to ask for permission. For software which is copyrighted by the Free 252 | Software Foundation, write to the Free Software Foundation; we sometimes 253 | make exceptions for this. Our decision will be guided by the two goals 254 | of preserving the free status of all derivatives of our free software and 255 | of promoting the sharing and reuse of software generally. 256 | 257 | NO WARRANTY 258 | 259 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 260 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 261 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 262 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 263 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 264 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 265 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 266 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 267 | REPAIR OR CORRECTION. 268 | 269 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 270 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 271 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 272 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 273 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 274 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 275 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 276 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 277 | POSSIBILITY OF SUCH DAMAGES. 278 | 279 | END OF TERMS AND CONDITIONS 280 | 281 | How to Apply These Terms to Your New Programs 282 | 283 | If you develop a new program, and you want it to be of the greatest 284 | possible use to the public, the best way to achieve this is to make it 285 | free software which everyone can redistribute and change under these terms. 286 | 287 | To do so, attach the following notices to the program. It is safest 288 | to attach them to the start of each source file to most effectively 289 | convey the exclusion of warranty; and each file should have at least 290 | the "copyright" line and a pointer to where the full notice is found. 291 | 292 | 293 | Copyright (C) 294 | 295 | This program is free software; you can redistribute it and/or modify 296 | it under the terms of the GNU General Public License as published by 297 | the Free Software Foundation; either version 2 of the License, or 298 | (at your option) any later version. 299 | 300 | This program is distributed in the hope that it will be useful, 301 | but WITHOUT ANY WARRANTY; without even the implied warranty of 302 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 303 | GNU General Public License for more details. 304 | 305 | You should have received a copy of the GNU General Public License 306 | along with this program; if not, write to the Free Software 307 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 308 | 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Library General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /quicktoggler@shihira.github.com/prefs.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 1000 7 | 3600000 8 | 500 9 | 10 10 | 11 | 12 | True 13 | False 14 | vertical 15 | 16 | 17 | 500 18 | True 19 | False 20 | 15 21 | 15 22 | 10 23 | 15 24 | vertical 25 | 26 | 27 | 10 28 | True 29 | False 30 | 20 31 | 32 | 33 | Custom Entries File 34 | True 35 | True 36 | False 37 | True 38 | 39 | 40 | False 41 | True 42 | 0 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 200 51 | True 52 | False 53 | 54 | 55 | 56 | False 57 | True 58 | end 59 | 2 60 | 61 | 62 | 63 | 64 | False 65 | True 66 | 0 67 | 68 | 69 | 70 | 71 | True 72 | False 73 | 10 74 | 75 | 76 | True 77 | False 78 | Indicator Icon 79 | 80 | 81 | False 82 | True 83 | 0 84 | 85 | 86 | 87 | 88 | 200 89 | True 90 | True 91 | 92 | 93 | False 94 | True 95 | end 96 | 1 97 | 98 | 99 | 100 | 101 | False 102 | True 103 | 1 104 | 105 | 106 | 107 | 108 | True 109 | False 110 | 10 111 | 112 | 113 | True 114 | False 115 | Indicator Text 116 | 117 | 118 | False 119 | True 120 | 0 121 | 122 | 123 | 124 | 125 | 200 126 | True 127 | True 128 | 129 | 130 | False 131 | True 132 | end 133 | 1 134 | 135 | 136 | 137 | 138 | False 139 | True 140 | 2 141 | 142 | 143 | 144 | 145 | True 146 | False 147 | 10 148 | 149 | 150 | True 151 | False 152 | vertical 153 | 154 | 155 | True 156 | False 157 | start 158 | Shortcut 159 | 160 | 161 | False 162 | True 163 | 0 164 | 165 | 166 | 167 | 168 | True 169 | False 170 | start 171 | Press to Edit 172 | 173 | 174 | 175 | 176 | 177 | False 178 | True 179 | 1 180 | 181 | 182 | 183 | 184 | False 185 | True 186 | 0 187 | 188 | 189 | 190 | 191 | New Accerlator... 192 | 200 193 | True 194 | True 195 | True 196 | 197 | 198 | False 199 | True 200 | end 201 | 1 202 | 203 | 204 | 205 | 206 | False 207 | True 208 | 3 209 | 210 | 211 | 212 | 213 | True 214 | False 215 | 10 216 | 217 | 218 | True 219 | False 220 | Detection Interval (ms) 221 | 222 | 223 | False 224 | True 225 | 0 226 | 227 | 228 | 229 | 230 | 200 231 | True 232 | True 233 | number 234 | adj-interval 235 | 236 | 237 | False 238 | True 239 | end 240 | 1 241 | 242 | 243 | 244 | 245 | False 246 | True 247 | 3 248 | 249 | 250 | 251 | 252 | True 253 | False 254 | 10 255 | 256 | 257 | True 258 | False 259 | Show Filter 260 | 261 | 262 | False 263 | True 264 | 0 265 | 266 | 267 | 268 | 269 | True 270 | True 271 | 272 | 273 | False 274 | True 275 | end 276 | 1 277 | 278 | 279 | 280 | 281 | False 282 | True 283 | 4 284 | 285 | 286 | 287 | 288 | True 289 | False 290 | 10 291 | 5 292 | 293 | 294 | True 295 | False 296 | Logging 297 | 298 | 299 | 300 | 301 | 302 | 303 | False 304 | True 305 | 0 306 | 307 | 308 | 309 | 310 | True 311 | True 312 | 313 | 314 | False 315 | True 316 | end 317 | 1 318 | 319 | 320 | 321 | 322 | False 323 | True 324 | 5 325 | 326 | 327 | 328 | 329 | True 330 | False 331 | 5 332 | 333 | 334 | False 335 | True 336 | 6 337 | 338 | 339 | 340 | 341 | True 342 | False 343 | 10 344 | 10 345 | vertical 346 | 347 | 348 | GNOME Default 349 | True 350 | True 351 | False 352 | start 353 | True 354 | True 355 | 356 | 357 | False 358 | True 359 | 1 360 | 361 | 362 | 363 | 364 | True 365 | False 366 | 367 | 368 | Log to File 369 | True 370 | True 371 | False 372 | True 373 | True 374 | radio-log-gnome 375 | 376 | 377 | False 378 | True 379 | 0 380 | 381 | 382 | 383 | 384 | 200 385 | True 386 | False 387 | 388 | 389 | 390 | False 391 | True 392 | end 393 | 2 394 | 395 | 396 | 397 | 398 | False 399 | True 400 | 2 401 | 402 | 403 | 404 | 405 | False 406 | True 407 | 7 408 | 409 | 410 | 411 | 412 | True 413 | False 414 | 5 415 | 416 | 417 | True 418 | False 419 | Notification 420 | 421 | 422 | 423 | 424 | 425 | 426 | False 427 | True 428 | 0 429 | 430 | 431 | 432 | 433 | True 434 | True 435 | 436 | 437 | False 438 | True 439 | end 440 | 1 441 | 442 | 443 | 444 | 445 | False 446 | True 447 | 8 448 | 449 | 450 | 451 | 452 | True 453 | False 454 | 5 455 | 456 | 457 | False 458 | True 459 | 9 460 | 461 | 462 | 463 | 464 | True 465 | False 466 | 10 467 | vertical 468 | 469 | 470 | Notify me when a process exited unexpectedly 471 | True 472 | True 473 | False 474 | start 475 | True 476 | 477 | 478 | False 479 | True 480 | 0 481 | 482 | 483 | 484 | 485 | Notify me when the extension failed to work normally 486 | True 487 | True 488 | False 489 | start 490 | True 491 | 492 | 493 | False 494 | True 495 | 1 496 | 497 | 498 | 499 | 500 | Notify me when the state of a toggler was changed 501 | True 502 | True 503 | False 504 | start 505 | True 506 | 507 | 508 | False 509 | True 510 | 2 511 | 512 | 513 | 514 | 515 | False 516 | True 517 | 10 518 | 519 | 520 | 521 | 522 | False 523 | True 524 | 0 525 | 526 | 527 | 528 | 529 | True 530 | False 531 | expand 532 | 533 | 534 | Apply 535 | True 536 | True 537 | True 538 | 539 | 540 | True 541 | True 542 | 0 543 | 544 | 545 | 546 | 547 | Restore 548 | True 549 | True 550 | True 551 | 552 | 553 | True 554 | True 555 | 1 556 | 557 | 558 | 559 | 560 | False 561 | True 562 | 1 563 | 564 | 565 | 566 | 567 | --------------------------------------------------------------------------------