├── .gitignore ├── pics └── screenshot.png ├── icons ├── icon (copy).png ├── icon (copy).svg ├── icon-exit-node_old.svg ├── icon (copy).svg.2022_08_11_13_17_01.0.svg ├── icon.svg ├── small.svg ├── icon-old.svg ├── big.svg └── icon-exit-node.svg ├── tailscale-status@maxgallup.github.com ├── schemas │ ├── gschemas.compiled │ └── org.gnome.shell.extensions.tailscale-status.gschema.xml ├── metadata.json ├── icon-up.svg ├── prefs.js ├── icon-down.svg ├── icon-exit-node.svg └── extension.js ├── docs └── notes.md ├── Makefile ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | tailscale-status@maxgallup.github.com.zip 2 | 3 | -------------------------------------------------------------------------------- /pics/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxgallup/tailscale-status/HEAD/pics/screenshot.png -------------------------------------------------------------------------------- /icons/icon (copy).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxgallup/tailscale-status/HEAD/icons/icon (copy).png -------------------------------------------------------------------------------- /tailscale-status@maxgallup.github.com/schemas/gschemas.compiled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxgallup/tailscale-status/HEAD/tailscale-status@maxgallup.github.com/schemas/gschemas.compiled -------------------------------------------------------------------------------- /tailscale-status@maxgallup.github.com/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "_generated": "Generated by SweetTooth, do not edit", 3 | "description": "Manage Tailscale connections and check status from desktop read more at https://github.com/maxgallup/tailscale-status/blob/main/README.md", 4 | "name": "Tailscale Status", 5 | "settings-schema": "org.gnome.shell.extensions.tailscale-status", 6 | "shell-version": ["45", "46", "47", "48", "49"], 7 | "url": "https://github.com/maxgallup/tailscale-status", 8 | "uuid": "tailscale-status@maxgallup.github.com", 9 | "version": 20 10 | } 11 | -------------------------------------------------------------------------------- /tailscale-status@maxgallup.github.com/schemas/org.gnome.shell.extensions.tailscale-status.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | "https://controlplane.tailscale.com" 6 | Login-Server Url 7 | If you are using Headscale for your control server, use your Headscale instance's URL. 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /icons/icon (copy).svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tailscale-status@maxgallup.github.com/icon-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/notes.md: -------------------------------------------------------------------------------- 1 | 2 | # Local API 3 | 4 | `curl --unix-socket /run/tailscale/tailscaled.sock http://localhost/` 5 | * "/localapi/v0/whois" 6 | * "/localapi/v0/goroutines" 7 | * "/localapi/v0/profile" 8 | * "/localapi/v0/status" 9 | * "/localapi/v0/logout" 10 | * "/localapi/v0/login-interactive" 11 | * "/localapi/v0/prefs" 12 | * "/localapi/v0/ping" 13 | * "/localapi/v0/check-prefs" 14 | * "/localapi/v0/check-ip-forwarding" 15 | * "/localapi/v0/bugreport" 16 | * "/localapi/v0/file-targets" 17 | * "/localapi/v0/set-dns" 18 | * "/localapi/v0/derpmap" 19 | * "/localapi/v0/metrics" 20 | * "/localapi/v0/debug" 21 | * "/localapi/v0/set-expiry-sooner" 22 | * "/localapi/v0/dial" 23 | * "/localapi/v0/id-token" 24 | 25 | # commands 26 | ``` bash 27 | #!/bin/bash 28 | 29 | DATA=$(curl --silent --unix-socket /run/tailscale/tailscaled.sock http://localhost/localapi/v0/status) 30 | 31 | BACKENDSTATE=$(echo "$DATA" | jq -r .BackendState) 32 | 33 | echo $BACKENDSTATE 34 | 35 | 36 | ``` 37 | 38 | 39 | # password-less command 40 | `tailscale up --operator=$USER || pkexec tailscale up --operator=$USER` 41 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Run this in a terminal window to see the logs from the extensions and if on X11 3 | # hit F2 and type "r" to restart gnome-shell to apply any changes. 4 | debug: 5 | journalctl -f -o cat /usr/bin/gnome-shell 6 | 7 | # Development in wayland is a bit nicer, this opens a separate gnome session 8 | test-wayland: 9 | env GNOME_SHELL_SLOWDOWN_FACTOR=2 MUTTER_DEBUG_DUMMY_MODE_SPECS=1920x1080 dbus-run-session -- gnome-shell --nested --wayland 10 | 11 | # Use this command to temporary install the extension. Note, it might be easier to rename 12 | # it since I've experienced some kind of caching or automated upgrading which would update 13 | # the extension to the latest version. 14 | link: 15 | cd $$PWD/tailscale-status@maxgallup.github.com && glib-compile-schemas schemas/ 16 | ln -s $$PWD/tailscale-status@maxgallup.github.com $$HOME/.local/share/gnome-shell/extensions/tailscale-status@maxgallup.github.com 17 | 18 | # Resulting zip used to submit to gnome extensions 19 | zip: 20 | cd tailscale-status@maxgallup.github.com && zip -r ../tailscale-status@maxgallup.github.com.zip * 21 | 22 | clean: 23 | rm tailscale-status@maxgallup.github.com.zip 24 | -------------------------------------------------------------------------------- /tailscale-status@maxgallup.github.com/prefs.js: -------------------------------------------------------------------------------- 1 | 2 | import Adw from 'gi://Adw'; 3 | import Gtk from 'gi://Gtk'; 4 | 5 | import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; 6 | 7 | export default class TailscaleStatusExtensionPreferences extends ExtensionPreferences { 8 | fillPreferencesWindow(window) { 9 | 10 | 11 | let settings = this.getSettings(); 12 | 13 | const page = new Adw.PreferencesPage(); 14 | const group = new Adw.PreferencesGroup(); 15 | 16 | page.add(group); 17 | 18 | const row = new Adw.ActionRow({ title: 'Login-Server URL' }); 19 | group.add(row); 20 | const textBox = new Gtk.Entry({ 21 | buffer: new Gtk.EntryBuffer() 22 | }); 23 | textBox.set_text(settings.get_string ('login-server')); 24 | textBox.connect("changed", function (w) { 25 | // .get_buffer().text 26 | settings.set_string('login-server',w.get_buffer().text) 27 | 28 | }); 29 | row.add_suffix(textBox); 30 | 31 | window.add(page); 32 | } 33 | } 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gnome Extension: tailscale-status 2 | **This extension is in no way affiliated with Tailscale Inc.** 3 | 4 | Easily manage your tailnets from a GUI gnome extension. 5 | Thus, this requires that you have **setup tailscale beforehand**. 6 | 7 | ![menu image](pics/screenshot.png) 8 | 9 | ### Compatibility: post-gnome45 10 | * Due to breaking changes addded in Gnome45, two versions of this extension will have to be supported: [pre-gnome45](https://github.com/maxgallup/tailscale-status/tree/pre-gnome45) and [post-gnome45](https://github.com/maxgallup/tailscale-status/tree/post-gnome45). **This branch is post-gnome45.** 11 | 12 | ### Features 13 | * Copy address of any node by clicking it in the menu 14 | * 💻 - your own computer 15 | * 🟢 - online or idle 16 | * ⚫ - offline 17 | * enable/disable incoming connections 18 | * accept/reject subnet routes 19 | * *if exit node:* allow direct access to local network 20 | * Accept or send files with taildrop 21 | * Connect through an available [exit node](https://tailscale.com/kb/1103/exit-nodes/) 22 | * Switch accounts 23 | * Set custom headscale server url via the preferences. 24 | 25 | ### Dependencies 26 | This obviously **requires** [tailscale](https://tailscale.com) to work! 27 | 28 | ### Installation 29 | Download the `tailscale-status@maxgallup.github.com` directory and move it to `~/.local/share/gnome-shell/extensions/`. 30 | Enable the extension in *Extensions* or *Extension Manager*. 31 | You might have to log in and out for the extension to be loaded. 32 | 33 | ### Contribute 34 | Sadly, we must maintain two separate branches for before and after gnome 45 due to breaking changes. Make pull requests to the correct respective branch. Additionally, please adhere to the [review guidlines](https://gjs.guide/extensions/review-guidelines/review-guidelines.html#basics) as much as possible. 35 | 36 | The Makefile includes useful targets for development. If running on wayland, use `make test-wayland` to open a nested gnome sessions. 37 | 38 | ### TODOs 39 | - [ ] Rewrite extension to utilize tailscale api instead of running `tailscale` commands. 40 | 41 | -------------------------------------------------------------------------------- /tailscale-status@maxgallup.github.com/icon-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 36 | 42 | 49 | 56 | 63 | 70 | 77 | 84 | 91 | 98 | 105 | 106 | -------------------------------------------------------------------------------- /icons/icon-exit-node_old.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 39 | 45 | 52 | 59 | 66 | 73 | 80 | 87 | 94 | 101 | 108 | 109 | -------------------------------------------------------------------------------- /icons/icon (copy).svg.2022_08_11_13_17_01.0.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 39 | 45 | 52 | 59 | 66 | 73 | 80 | 87 | 94 | 101 | 108 | 109 | -------------------------------------------------------------------------------- /icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 51 | 57 | 63 | 69 | 75 | 81 | 87 | 93 | 99 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /icons/small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 51 | 57 | 63 | 69 | 75 | 81 | 87 | 93 | 99 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /icons/icon-old.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 51 | 57 | 63 | 69 | 75 | 81 | 87 | 93 | 99 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /icons/big.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 36 | 38 | 42 | 51 | 57 | 63 | 69 | 75 | 81 | 87 | 93 | 99 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /icons/icon-exit-node.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 20 | 24 | 25 | 26 | 45 | 54 | 63 | 69 | 75 | 82 | 89 | 95 | 101 | 108 | 115 | 122 | 123 | -------------------------------------------------------------------------------- /tailscale-status@maxgallup.github.com/icon-exit-node.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 20 | 24 | 25 | 26 | 45 | 54 | 63 | 69 | 75 | 82 | 89 | 95 | 101 | 108 | 115 | 122 | 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 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 Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /tailscale-status@maxgallup.github.com/extension.js: -------------------------------------------------------------------------------- 1 | import * as Main from 'resource:///org/gnome/shell/ui/main.js'; 2 | import St from 'gi://St'; 3 | import Clutter from 'gi://Clutter'; 4 | 5 | import * as ExtensionUtils from 'resource:///org/gnome/shell/misc/extensionUtils.js'; 6 | import * as Util from 'resource:///org/gnome/shell/misc/util.js'; 7 | 8 | import GObject from 'gi://GObject'; 9 | import GLib from 'gi://GLib'; 10 | import Gio from 'gi://Gio'; 11 | 12 | import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; 13 | import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; 14 | 15 | import { Extension, gettext as _ } from "resource:///org/gnome/shell/extensions/extension.js"; 16 | 17 | 18 | const statusString = "Status: "; 19 | const enabledString = "🟢"; 20 | const disabledString = "⚫"; 21 | const ownConnectionString = "💻"; 22 | 23 | class TailscaleNode { 24 | /** 25 | * @param {boolean} _isMullvadExitNode 26 | * @param {string[]} _groupPath - e.g. ["Mullvad", "Norway", "Oslo"] 27 | */ 28 | constructor(_name, _address, _online, _offersExit, _usesExit, _isSelf, _isMullvadExitNode, _groupPath) { 29 | this.name = _name; 30 | this.address = _address; 31 | this.online = _online; 32 | this.offersExit = _offersExit; 33 | this.usesExit = _usesExit; 34 | this.isSelf = _isSelf; 35 | /** We probably want to ignore these for anything that's not picking an exit node. */ 36 | this.isMullvadExitNode = _isMullvadExitNode; 37 | /** Currently just used to group the Mullvad exit nodes, but code is structured to take arbitrary groupings. */ 38 | this.groupPath = _groupPath; 39 | } 40 | 41 | get line() { 42 | var statusIcon; 43 | if (this.isSelf) { 44 | statusIcon = ownConnectionString; 45 | } else if (this.online) { 46 | statusIcon = enabledString; 47 | } else { 48 | statusIcon = disabledString; 49 | } 50 | return statusIcon + " " + this.address + " " + this.name; 51 | } 52 | } 53 | 54 | /** @type {TailscaleNode[]} */ 55 | let nodes = []; 56 | /** @typedef {{nodes: TailscaleNode[], subTrees: {[k: string]: NodesTree}}} NodesTree */ 57 | /** @type {NodesTree} */ 58 | let nodesTree = { nodes: [], subTrees: {} } 59 | let accounts = []; 60 | let currentAccount = "(click Update Accounts List)"; 61 | 62 | let nodesMenu; 63 | let accountButton; 64 | let accountsMenu; 65 | let accountIndicator; 66 | let logoutButton; 67 | let exitNodeMenu; 68 | let sendMenu; 69 | let statusItem; 70 | let authItem; 71 | let needToAuth = true; 72 | let authUrl; 73 | 74 | let health; 75 | 76 | let receiveFilesItem 77 | let shieldItem; 78 | let acceptRoutesItem; 79 | let allowLanItem; 80 | let statusSwitchItem; 81 | let downloads_path = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOWNLOAD); 82 | let icon; 83 | let icon_down; 84 | let icon_up; 85 | let icon_exit_node; 86 | let SETTINGS; 87 | 88 | 89 | function myWarn(string) { 90 | console.log("🟡 [tailscale-status]: " + string); 91 | } 92 | 93 | function myError(string) { 94 | console.log("🔴 [tailscale-status]: " + string); 95 | } 96 | 97 | 98 | function extractNodeInfo(json) { 99 | nodes = []; 100 | nodesTree = { nodes: [], subTrees: {} }; 101 | 102 | var me = json.Self; 103 | if (me.TailscaleIPs != null) { 104 | nodes.push(new TailscaleNode( 105 | me.DNSName.split(".")[0], 106 | me.TailscaleIPs[0], 107 | me.Online, 108 | me.ExitNodeOption, 109 | me.ExitNode, 110 | true, 111 | false, 112 | [] 113 | ) 114 | ); 115 | } 116 | for (let p in json.Peer) { 117 | var n = json.Peer[p]; 118 | let isMullvad = false; 119 | let groupPath = []; 120 | // We special-case these guys. Tailscale clients sometimes refer to "Location-based exit nodes", 121 | // perhaps in future it should be done by nodes with a .Location instead? 122 | if (n.Tags?.includes('tag:mullvad-exit-node')) { 123 | isMullvad = true; 124 | if (n.Location?.Country && n.Location?.City) { 125 | groupPath = ["Mullvad", n.Location.Country, n.Location.City]; 126 | } else { 127 | groupPath = ["Mullvad"] 128 | } 129 | } 130 | if (n.TailscaleIPs != null) { 131 | nodes.push(new TailscaleNode( 132 | n.DNSName.split(".")[0], 133 | n.TailscaleIPs[0], 134 | n.Online, 135 | n.ExitNodeOption, 136 | n.ExitNode, 137 | false, 138 | isMullvad, 139 | groupPath 140 | )); 141 | } 142 | 143 | } 144 | nodes.sort(combineSort(sortProp('isSelf'), sortProp('online', 'desc'), sortArrProp('groupPath'), sortProp('name'))) 145 | 146 | for (const n of nodes) { 147 | let t = nodesTree; 148 | // recurse into / initialize the tree, one level per entry in groupPath 149 | for (const p of n.groupPath) { 150 | if (!(p in t.subTrees)) { 151 | t.subTrees[p] = { nodes: [], subTrees: {} } 152 | } 153 | t = t.subTrees[p] 154 | } 155 | t.nodes.push(n); 156 | } 157 | } 158 | 159 | function sortArrProp(p) { 160 | return function comp(a, b) { 161 | const [_aa, _bb] = [a[p] ?? [], b[p] ?? []] 162 | for (let i = 0; i < Math.max(_aa.length, _bb.length); i++) { 163 | const [_a, _b] = [_aa[i], _bb[i]] 164 | if (_a < _b) { 165 | return -1; 166 | } else if (_b < _a) { 167 | return 1; 168 | } else { 169 | continue; 170 | } 171 | } 172 | } 173 | } 174 | /** @param {'desc' | undefined} desc - descending sort */ 175 | function sortProp(p, desc=undefined) { 176 | return function comp(a, b) { 177 | if (desc == 'desc') { 178 | [b, a] = [a, b]; 179 | } 180 | const [_a, _b] = [a[p], b[p]]; 181 | if (_a < _b) { 182 | return -1; 183 | } else if (_b < _a) { 184 | return 1; 185 | } else { 186 | return 0; 187 | } 188 | } 189 | } 190 | function combineSort(...sorters) { 191 | return function comp(a, b) { 192 | for (const fn of sorters) { 193 | const res = fn(a, b); 194 | if (res != 0) { 195 | return res 196 | } 197 | // else this sorter considers them equal, try the next one. 198 | } 199 | } 200 | } 201 | function getUsername(json) { 202 | let id = 0 203 | if (json.Self.UserID != null) { 204 | id = json.Self.UserID 205 | } 206 | if (json.User != null) { 207 | for (const [key, value] of Object.entries(json.User)) { 208 | if (value.ID === id) { 209 | return value.LoginName 210 | } 211 | } 212 | } 213 | return json.Self.HostName 214 | } 215 | function setStatus(json) { 216 | authItem.label.text = "Logged in: " + getUsername(json); 217 | accountIndicator.label.text = "Account: " + currentAccount; 218 | authItem.sensitive = false; 219 | health = json.Health 220 | switch (json.BackendState) { 221 | case "Running": 222 | needToAuth = true 223 | icon.gicon = icon_up; 224 | statusSwitchItem.setToggleState(true); 225 | statusItem.label.text = statusString + "up (no exit-node)"; 226 | nodes.forEach((node) => { 227 | if (node.usesExit) { 228 | statusItem.label.text = statusString + "up (exit-node: " + node.name + ")"; 229 | icon.gicon = icon_exit_node; 230 | } 231 | }) 232 | setAllItems(true); 233 | break; 234 | case "Stopped": 235 | needToAuth = true 236 | icon.gicon = icon_down; 237 | statusSwitchItem.setToggleState(false); 238 | statusItem.label.text = statusString + "down"; 239 | nodes = []; 240 | setAllItems(false); 241 | statusSwitchItem.sensitive = true; 242 | break; 243 | case "NeedsLogin": 244 | icon.gicon = icon_down; 245 | statusSwitchItem.setToggleState(false); 246 | authUrl = json.AuthURL; 247 | if (authUrl.length > 0 && needToAuth) { 248 | Util.spawn(['xdg-open', authUrl]) 249 | needToAuth = false 250 | } 251 | 252 | authItem.sensitive = true; 253 | statusItem.label.text = statusString + "needs login"; 254 | authItem.label.text = "Click to Login" 255 | 256 | setAllItems(false); 257 | nodes = []; 258 | break; 259 | 260 | default: 261 | myError("Error: unknown state"); 262 | } 263 | } 264 | 265 | function setAllItems(b) { 266 | shieldItem.sensitive = b; 267 | acceptRoutesItem.sensitive = b; 268 | allowLanItem.sensitive = b; 269 | statusSwitchItem.sensitive = b; 270 | receiveFilesItem.sensitive = b; 271 | nodesMenu.sensitive = b; 272 | sendMenu.sensitive = b; 273 | exitNodeMenu.sensitive = b; 274 | accountsMenu.sensitive = b; 275 | accountButton.sensitive = b; 276 | logoutButton.sensitive = b; 277 | } 278 | 279 | 280 | 281 | function refreshNodesMenu() { 282 | nodesMenu.menu.removeAll(); 283 | for (const node of nodes) { 284 | if (node.isMullvadExitNode) { 285 | continue; 286 | } 287 | 288 | let item = new PopupMenu.PopupMenuItem(node.line) 289 | item.connect('activate', () => { 290 | St.Clipboard.get_default().set_text(St.ClipboardType.CLIPBOARD, node.address); 291 | Main.notify("Copied " + node.address + " to clipboard! (" + node.name + ")"); 292 | }); 293 | nodesMenu.menu.addMenuItem(item); 294 | }; 295 | } 296 | 297 | /** 298 | * This is a PopupSubMenuMenuItem with some patches to make nested submenus work, 299 | * by default they don't work at all. 300 | */ 301 | const FixedSubMenuMenuItem = GObject.registerClass( 302 | class FixedSubMenuMenuItem extends PopupMenu.PopupSubMenuMenuItem { 303 | _init(name, rootScroller) { 304 | super._init(name); 305 | this.rootScroller = rootScroller; 306 | 307 | // Monkey-patch scrolling - we'll leave scrolling to the rootScroller. 308 | // Disable scrolling on our own menu's ScrollBox. 309 | this.menu._needsScrollbar = () => false; 310 | this.menu.actor.set_mouse_scrolling(false); 311 | } 312 | 313 | _subMenuOpenStateChanged(menu, open) { 314 | super._subMenuOpenStateChanged(menu, open); 315 | 316 | // we've changed the height of a submenu. Gnome doesn't handle this properly, 317 | // so we need to go and tell the rootScroller that its height has changed. 318 | // Copy-paste from PopupSubMenu.open(). 319 | { 320 | const needsScrollbar = this.rootScroller._needsScrollbar(); 321 | 322 | this.rootScroller.actor.vscrollbar_policy = St.PolicyType.ALWAYS; 323 | 324 | if (needsScrollbar) 325 | this.rootScroller.actor.add_style_pseudo_class('scrolled'); 326 | else 327 | this.rootScroller.actor.remove_style_pseudo_class('scrolled'); 328 | } 329 | 330 | } 331 | } 332 | ); 333 | 334 | /** 335 | * @param {PopupMenu.PopupMenuBase} menu 336 | * @param {NodesTree} t 337 | * @param {string} indent 338 | * @param {PopupMenu.PopupMenuBase | null} rootScroller 339 | * we need to keep track of the ExitNodes popupmenu so we can fix Gnome's buggy handling of nested 340 | * submenus. 341 | */ 342 | function _refreshExitNodesMenu(menu, t, indent = '', rootScroller = null) { 343 | let usesExit = false; 344 | 345 | // Add any nodes to this level of the tree 346 | for (const node of t.nodes) { 347 | if (!node.offersExit) { 348 | continue; 349 | } 350 | 351 | const item = new PopupMenu.PopupMenuItem(indent+node.name) 352 | item.connect('activate', () => { 353 | cmdTailscale({ args: ["up", "--exit-node=" + node.address, "--reset"] }) 354 | }); 355 | item.setOrnament(node.usesExit ? 1 : 0) 356 | menu.addMenuItem(item); 357 | usesExit ||= node.usesExit; 358 | } 359 | 360 | rootScroller = rootScroller || menu; 361 | 362 | // Add any subtress to this level of the tree 363 | for (const [name, st] of Object.entries(t.subTrees)) { 364 | const subMenu = new FixedSubMenuMenuItem(indent+name, rootScroller); 365 | 366 | const stUsesExit = _refreshExitNodesMenu(subMenu.menu, st, indent+' ', rootScroller) 367 | 368 | subMenu.setOrnament(stUsesExit ? 1 : 0) 369 | menu.addMenuItem(subMenu) 370 | usesExit ||= stUsesExit 371 | } 372 | 373 | return usesExit 374 | } 375 | 376 | function refreshExitNodesMenu() { 377 | exitNodeMenu.menu.removeAll(); 378 | 379 | const usesExit = _refreshExitNodesMenu(exitNodeMenu.menu, nodesTree); 380 | 381 | var noneItem = new PopupMenu.PopupMenuItem('None'); 382 | noneItem.connect('activate', () => { 383 | cmdTailscale({ args: ["up", "--exit-node=", "--reset"] }); 384 | }); 385 | noneItem.setOrnament(usesExit ? 0 : 1) 386 | exitNodeMenu.menu.addMenuItem(noneItem, 0); 387 | } 388 | 389 | function refreshSendMenu() { 390 | sendMenu.menu.removeAll(); 391 | for (const node of nodes) { 392 | if (!node.online || node.isSelf || node.isMullvadExitNode) { 393 | continue; 394 | } 395 | 396 | var item = new PopupMenu.PopupMenuItem(node.name) 397 | item.connect('activate', () => { 398 | sendFiles(node.address); 399 | }); 400 | sendMenu.menu.addMenuItem(item); 401 | } 402 | } 403 | 404 | function sendFiles(dest) { 405 | try { 406 | let proc = Gio.Subprocess.new( 407 | ["zenity", "--file-selection", "--multiple"], 408 | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE 409 | ); 410 | proc.communicate_utf8_async(null, null, (proc, res) => { 411 | try { 412 | let [, stdout, stderr] = proc.communicate_utf8_finish(res); 413 | if (proc.get_successful()) { 414 | if (stdout != '') { 415 | files = stdout.trim().split("|") 416 | cmdTailscaleFile(files, dest) 417 | } 418 | } else { 419 | myError("zenity failed"); 420 | } 421 | } catch (e) { 422 | myError(e); 423 | } 424 | }); 425 | } catch (e) { 426 | myError(e); 427 | } 428 | } 429 | 430 | 431 | function cmdTailscaleSwitchList(unprivileged = true) { 432 | let args = ["switch", "--list"] 433 | let command = (unprivileged ? ["tailscale"] : ["pkexec", "tailscale"]).concat(args); 434 | 435 | try { 436 | let proc = Gio.Subprocess.new( 437 | command, 438 | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE 439 | ); 440 | proc.communicate_utf8_async(null, null, (proc, res) => { 441 | try { 442 | let [, stdout, stderr] = proc.communicate_utf8_finish(res); 443 | if (proc.get_successful()) { 444 | accounts = stdout.split("\n") 445 | accounts = accounts.filter((item) => item.length > 0) 446 | accountsMenu.menu.removeAll() 447 | accounts.forEach((account) => { 448 | if (account.slice(-2) == " *") { 449 | account = account.slice(0, -2) 450 | currentAccount = account 451 | } 452 | let accountItem = new PopupMenu.PopupMenuItem(account) 453 | accountItem.connect('activate', () => { 454 | // find the mail address in the account string 455 | const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/; 456 | const email = account.match(emailRegex); 457 | if (email == null) { 458 | myError("failed to extract email from account string") 459 | return 460 | } 461 | cmdTailscaleSwitch(email[0]); 462 | }); 463 | accountsMenu.menu.addMenuItem(accountItem); 464 | }); 465 | } else { 466 | if (unprivileged) { 467 | myWarn("retrying tailscale switch --list") 468 | cmdTailscaleSwitchList(false) 469 | } else { 470 | myError("cmd 'tailscale switch --list' failed") 471 | } 472 | } 473 | } catch (e) { 474 | myError(e); 475 | } 476 | }); 477 | } catch (e) { 478 | myError(e); 479 | } 480 | } 481 | 482 | function cmdTailscaleSwitch(account) { 483 | if (currentAccount == account) { 484 | Main.notify("Already logged in with " + account) 485 | return 486 | } else { 487 | Main.notify("Switching to " + account) 488 | currentAccount = account 489 | } 490 | 491 | cmdTailscale({ 492 | args: ["switch", account], 493 | addLoginServer: false 494 | }) 495 | 496 | } 497 | 498 | function cmdTailscaleStatus() { 499 | try { 500 | let proc = Gio.Subprocess.new( 501 | // ["curl", "--silent", "--unix-socket", "/run/tailscale/tailscaled.sock", "http://localhost/localapi/v0/status" ], 502 | ["tailscale", "status", "--json"], 503 | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE 504 | ); 505 | proc.communicate_utf8_async(null, null, (proc, res) => { 506 | 507 | try { 508 | let [, stdout, stderr] = proc.communicate_utf8_finish(res); 509 | if (proc.get_successful()) { 510 | const j = JSON.parse(stdout); 511 | extractNodeInfo(j); 512 | setStatus(j); 513 | refreshExitNodesMenu(); 514 | refreshSendMenu(); 515 | refreshNodesMenu(); 516 | } 517 | } catch (e) { 518 | myError(e); 519 | } 520 | }); 521 | } catch (e) { 522 | myError(e); 523 | } 524 | } 525 | 526 | function cmdTailscale({args, unprivileged = true, addLoginServer = true}) { 527 | let original_args = args 528 | 529 | if (addLoginServer) { 530 | args = args.concat(["--login-server=" + SETTINGS.get_string('login-server')]) 531 | } 532 | 533 | let command = (unprivileged ? ["tailscale"] : ["pkexec", "tailscale"]).concat(args); 534 | 535 | try { 536 | let proc = Gio.Subprocess.new( 537 | command, 538 | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE 539 | ); 540 | proc.communicate_utf8_async(null, null, (proc, res) => { 541 | try { 542 | proc.communicate_utf8_finish(res); 543 | if (!proc.get_successful()) { 544 | if (unprivileged) { 545 | cmdTailscale({ 546 | args: args[0] == "up" ? original_args.concat(["--operator=" + GLib.get_user_name(), "--reset"]) : original_args, 547 | unprivileged: false, 548 | addLoginServer: addLoginServer 549 | }) 550 | } else { 551 | myWarn("failed @ cmdTailscale"); 552 | } 553 | } else { 554 | cmdTailscaleStatus() 555 | } 556 | } catch (e) { 557 | myError(e); 558 | } 559 | }); 560 | } catch (e) { 561 | myError(e); 562 | } 563 | } 564 | 565 | function cmdTailscaleRecFiles() { 566 | try { 567 | let proc = Gio.Subprocess.new( 568 | ["pkexec", "tailscale", "file", "get", downloads_path], 569 | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE 570 | ); 571 | proc.communicate_utf8_async(null, null, (proc, res) => { 572 | try { 573 | proc.communicate_utf8_finish(res); 574 | if (proc.get_successful()) { 575 | Main.notify('Saved files to ' + downloads_path); 576 | } else { 577 | Main.notify('Unable to receive files to ' + downloads_path, 'check logs with journalctl -f -o cat /usr/bin/gnome-shell'); 578 | myWarn("failed to accept files to " + downloads_path) 579 | } 580 | } catch (e) { 581 | myError(e); 582 | } 583 | }); 584 | } catch (e) { 585 | myError(e); 586 | } 587 | } 588 | 589 | const TailscalePopup = GObject.registerClass( 590 | class TailscalePopup extends PanelMenu.Button { 591 | 592 | _init(dir_path) { 593 | super._init(0); 594 | 595 | icon_down = Gio.icon_new_for_string(dir_path + '/icon-down.svg'); 596 | icon_up = Gio.icon_new_for_string(dir_path + '/icon-up.svg'); 597 | icon_exit_node = Gio.icon_new_for_string(dir_path + '/icon-exit-node.svg'); 598 | 599 | icon = new St.Icon({ 600 | gicon: icon_down, 601 | style_class: 'system-status-icon', 602 | }); 603 | 604 | this.add_child(icon); 605 | 606 | this.menu.connect('open-state-changed', (menu, open) => { 607 | if (open) { 608 | cmdTailscaleStatus(); 609 | } 610 | }); 611 | 612 | // monkey-patch to nuke this property - it's buggy, if submenus are in a tree, 613 | // then it causes the parent to close when a child is opened, even though the parent 614 | // should stay open so you can see the child! 615 | this.menu._setOpenedSubMenu = () => {};`` 616 | 617 | // ------ MAIN STATUS ITEM ------ 618 | statusItem = new PopupMenu.PopupMenuItem(statusString, { reactive: false }); 619 | 620 | // ------ AUTH ITEM ------ 621 | authItem = new PopupMenu.PopupMenuItem("Logged in", false); 622 | 623 | authItem.connect('activate', () => { 624 | cmdTailscaleStatus() 625 | if (authUrl.length == 0) { 626 | try { 627 | cmdTailscale({ 628 | args: ["up"], 629 | }); 630 | } catch (e) { 631 | myError(e); 632 | } 633 | } 634 | }); 635 | 636 | 637 | // ------ ACCOUNT INDICATOR ------ 638 | accountIndicator = new PopupMenu.PopupMenuItem("Account: ", { reactive: false}); 639 | 640 | // ------ MAIN SWITCH ------ 641 | statusSwitchItem = new PopupMenu.PopupSwitchMenuItem("Tailscale", false); 642 | statusSwitchItem.connect('activate', () => { 643 | if (statusSwitchItem.state) { 644 | cmdTailscale({ args: ["up"] }); 645 | } else { 646 | cmdTailscale({ 647 | args: ["down"], 648 | addLoginServer: false 649 | }); 650 | } 651 | }) 652 | 653 | // ------ UPDATE ACCOUNTS ------ 654 | accountButton = new PopupMenu.PopupMenuItem("Update Accounts List"); 655 | accountButton.connect('activate', (item) => { 656 | cmdTailscaleSwitchList() 657 | }) 658 | 659 | // ------ ACCOUNTS ------ 660 | accountsMenu = new PopupMenu.PopupSubMenuMenuItem("Accounts"); 661 | 662 | // ------ NODES ------ 663 | nodesMenu = new PopupMenu.PopupSubMenuMenuItem("Nodes"); 664 | nodes.forEach((node) => { 665 | nodesMenu.menu.addMenuItem(new PopupMenu.PopupMenuItem(node.line)); 666 | }); 667 | 668 | // ------ SHIELD ------ 669 | shieldItem = new PopupMenu.PopupSwitchMenuItem("Block Incoming", false); 670 | shieldItem.connect('activate', () => { 671 | if (shieldItem.state) { 672 | cmdTailscale({ args: ["up", "--shields-up"] }); 673 | } else { 674 | cmdTailscale({ args: ["up", "--shields-up=false", "--reset"] }); 675 | } 676 | }) 677 | 678 | 679 | // ------ ACCEPT ROUTES ------ 680 | acceptRoutesItem = new PopupMenu.PopupSwitchMenuItem("Accept Routes", false); 681 | acceptRoutesItem.connect('activate', () => { 682 | if (acceptRoutesItem.state) { 683 | cmdTailscale({ args: ["up", "--accept-routes"] }); 684 | } else { 685 | cmdTailscale({ args: ["up", "--accept-routes=false", "--reset"] }); 686 | } 687 | }) 688 | 689 | // ------ ALLOW DIRECT LAN ACCESS ------ 690 | allowLanItem = new PopupMenu.PopupSwitchMenuItem("Allow Direct Lan Access", false); 691 | allowLanItem.connect('activate', () => { 692 | if (allowLanItem.state) { 693 | if (nodes[0].usesExit) { 694 | cmdTailscale({ args: ["up", "--exit-node-allow-lan-access"] }); 695 | } else { 696 | Main.notify("Must setup exit node first"); 697 | allowLanItem.setToggleState(false); 698 | } 699 | } else { 700 | cmdTailscale({ args: ["up", "--exit-node-allow-lan-access=false", "--reset"] }); 701 | } 702 | }) 703 | 704 | // ------ RECEIVE FILES MENU ------ 705 | receiveFilesItem = new PopupMenu.PopupMenuItem("Accept incoming files"); 706 | receiveFilesItem.connect('activate', () => { 707 | cmdTailscaleRecFiles(); 708 | }) 709 | 710 | // ------ SEND FILES MENU ------ 711 | sendMenu = new PopupMenu.PopupSubMenuMenuItem("Send Files"); 712 | 713 | // ------ EXIT NODES ------- 714 | exitNodeMenu = new PopupMenu.PopupSubMenuMenuItem("Exit Nodes"); 715 | 716 | // ------ LOG OUT ------- 717 | logoutButton = new PopupMenu.PopupMenuItem("Log Out"); 718 | logoutButton.connect('activate', () => { 719 | cmdTailscale({ 720 | args: ["logout"], 721 | addLoginServer: false, 722 | }); 723 | }) 724 | 725 | // ------ ABOUT MENU------ 726 | let aboutMenu = new PopupMenu.PopupSubMenuMenuItem("About"); 727 | let healthMenu = new PopupMenu.PopupMenuItem("Health") 728 | healthMenu.connect('activate', () => { 729 | if (health != null) { 730 | Main.notify(health.join()); 731 | 732 | } else { 733 | Main.notify("null"); 734 | } 735 | }) 736 | let infoMenu = new PopupMenu.PopupMenuItem("This extension is in no way affiliated with Tailscale Inc.") 737 | let contributeMenu = new PopupMenu.PopupMenuItem("Contribute") 738 | contributeMenu.connect('activate', () => { 739 | Util.spawn(['xdg-open', "https://github.com/maxgallup/tailscale-status#contribute"]) 740 | }) 741 | 742 | 743 | // Order Matters! 744 | this.menu.addMenuItem(statusSwitchItem); 745 | this.menu.addMenuItem(statusItem); 746 | this.menu.addMenuItem(authItem); 747 | this.menu.addMenuItem(accountIndicator); 748 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); 749 | this.menu.addMenuItem(nodesMenu); 750 | this.menu.addMenuItem(accountButton); 751 | this.menu.addMenuItem(accountsMenu); 752 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); 753 | this.menu.addMenuItem(shieldItem); 754 | this.menu.addMenuItem(acceptRoutesItem); 755 | this.menu.addMenuItem(allowLanItem); 756 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); 757 | this.menu.addMenuItem(receiveFilesItem); 758 | this.menu.addMenuItem(sendMenu); 759 | this.menu.addMenuItem(exitNodeMenu); 760 | this.menu.addMenuItem(logoutButton); 761 | this.menu.addMenuItem(aboutMenu); 762 | aboutMenu.menu.addMenuItem(infoMenu); 763 | aboutMenu.menu.addMenuItem(contributeMenu); 764 | aboutMenu.menu.addMenuItem(healthMenu); 765 | } 766 | } 767 | ); 768 | 769 | 770 | 771 | let tailscale; 772 | 773 | 774 | export default class TailscaleStatusExtension extends Extension { 775 | enable() { 776 | SETTINGS = this.getSettings('org.gnome.shell.extensions.tailscale-status'); 777 | 778 | cmdTailscaleStatus() 779 | 780 | tailscale = new TailscalePopup(this.path); 781 | Main.panel.addToStatusArea('tailscale', tailscale, 1); 782 | } 783 | 784 | disable() { 785 | 786 | tailscale.destroy(); 787 | tailscale = null; 788 | SETTINGS = null; 789 | accounts = []; 790 | nodes = []; 791 | currentAccount = null; 792 | nodesMenu = null; 793 | accountButton = null; 794 | accountsMenu = null; 795 | accountIndicator = null; 796 | logoutButton = null; 797 | exitNodeMenu = null; 798 | sendMenu = null; 799 | statusItem = null; 800 | authItem = null; 801 | needToAuth = true; 802 | authUrl = null; 803 | 804 | health = null; 805 | 806 | receiveFilesItem = null; 807 | shieldItem = null; 808 | acceptRoutesItem = null; 809 | allowLanItem = null; 810 | statusSwitchItem = null; 811 | downloads_path = null; 812 | icon = null; 813 | icon_down = null; 814 | icon_up = null; 815 | icon_exit_node = null; 816 | 817 | } 818 | } 819 | --------------------------------------------------------------------------------