├── .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 |
13 |
--------------------------------------------------------------------------------
/tailscale-status@maxgallup.github.com/icon-up.svg:
--------------------------------------------------------------------------------
1 |
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 | 
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 |
106 |
--------------------------------------------------------------------------------
/icons/icon-exit-node_old.svg:
--------------------------------------------------------------------------------
1 |
2 |
109 |
--------------------------------------------------------------------------------
/icons/icon (copy).svg.2022_08_11_13_17_01.0.svg:
--------------------------------------------------------------------------------
1 |
2 |
109 |
--------------------------------------------------------------------------------
/icons/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
107 |
--------------------------------------------------------------------------------
/icons/small.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
107 |
--------------------------------------------------------------------------------
/icons/icon-old.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
107 |
--------------------------------------------------------------------------------
/icons/big.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
107 |
--------------------------------------------------------------------------------
/icons/icon-exit-node.svg:
--------------------------------------------------------------------------------
1 |
2 |
123 |
--------------------------------------------------------------------------------
/tailscale-status@maxgallup.github.com/icon-exit-node.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------