├── LICENSE ├── Makefile ├── README.rst ├── extension.js ├── metadata.json ├── notes.rst ├── prefs.js ├── schemas ├── gschemas.compiled └── org.gnome.shell.extensions.nothing-to-say.gschema.xml ├── screenshot-osd.png ├── screenshot-top-bar.png └── sounds ├── off.ogg └── on.ogg /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: zip compile-schemas 2 | 3 | zip: compile-schemas 4 | zip "gnome-shell-extension-nothing-to-say-$$(date +%Y%m%d%H%M%S)-$$(git rev-parse --short @).zip" \ 5 | *.js \ 6 | metadata.json \ 7 | sounds/*.ogg \ 8 | schemas/gschemas.compiled \ 9 | schemas/org.gnome.shell.extensions.nothing-to-say.gschema.xml 10 | 11 | compile-schemas: 12 | glib-compile-schemas --strict schemas/ 13 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | nothing to say 3 | ============== 4 | 5 | this gnome-shell extension always keeps your microphone muted, unless 6 | you actually have something to say. 7 | 8 | tl;dr: 9 | 10 | * microphone icon in the top bar, only visible when recording is active 11 | * one-click mute/unmute using the icon 12 | * keyboard shortcut to mute/unmute, with push-to-talk 13 | * osd and sound notifications for microphone events 14 | * install from https://extensions.gnome.org/extension/1113/nothing-to-say/ 15 | 16 | like it? you can `buy me a coffee `_! ☕🙏 17 | 18 | 19 | pics? 20 | ===== 21 | 22 | this is how it looks in the top bar: 23 | 24 | .. image:: /screenshot-top-bar.png?raw=true 25 | :alt: top bar screenshot 26 | :align: center 27 | 28 | this is the osd (on screen display) notification: 29 | 30 | .. image:: /screenshot-osd.png?raw=true 31 | :alt: osd screenshot 32 | :align: center 33 | 34 | 35 | for whom? 36 | ========= 37 | 38 | this extension is intended for gnome users who participate in 39 | teleconferences. 40 | 41 | it is especially awesome if you are in a noisy environment. you know, 42 | those coffee bars where the hipster crowd sits with their laptops. 43 | sipping from a way too expensive soy latte macchiato. which is served 44 | with a complimentary slice of gluten-free cake. which happens to be 45 | smaller than your finger nail. and it does not even taste sweet. 46 | anyway, i digress. 47 | 48 | is this you? great. read on. 49 | 50 | not you? well, maybe you are in a less exciting, but perhaps more 51 | common, open-plan office. 52 | 53 | is this you? totally cool. read on. 54 | 55 | 56 | what? 57 | ===== 58 | 59 | this extension offers these amazing features: 60 | 61 | * microphone icon in the top bar 62 | 63 | the icon shows whether the mic is muted or not. click it to toggle. 64 | the icon is only visible when the microphone is actually being 65 | recorded. that means no visual clutter if the microphone is not in 66 | use. 67 | 68 | * shortcut key to mute or unmute 69 | 70 | press the shortcut key once to unmute, and once again to mute. 71 | 72 | but there is more. the shortcut key also functions as a 73 | walkie-talkie style push-to-talk button. how cool is that? 74 | 75 | you do not know what that is? no worries, it is rather simple. press 76 | the configured shortcut key to unmute the microphone, and keep it 77 | pressed. whenever you release the shortcut key, the microphone will 78 | be muted again. so as long as you press the key you can talk, and as 79 | soon as you release it, you can cough and sneeze as much as you 80 | like. 81 | 82 | the default shortcut is ``backslash``. you don’t like it? 83 | funny, neither do i. but at least it does not clash with anything 84 | else, so please do not complain about it. why not? well, because you 85 | can change it in the preference pane! 86 | 87 | you can even add additional shortcuts. this involves setting the 88 | appropriate dconf key. the easiest way is typing this into a 89 | terminal window:: 90 | 91 | dconf write /org/gnome/shell/extensions/nothing-to-say/keybinding-toggle-mute '["backslash", "Pause"]' 92 | 93 | of course you should change the preferred shortcuts into something 94 | that makes sense for you and your keyboard. 95 | 96 | * on screen display (osd) pop-up notifications 97 | 98 | an osd pop-up, which is the small overlay window that also pops up 99 | when you change your speaker volume or laptop screen brightness, 100 | will be shown for the following events: 101 | 102 | * microphone (de)activation 103 | 104 | this happens when a video conferencing application starts or stops 105 | recording. 106 | 107 | * microphone muting and unmuting 108 | 109 | this happens when you mute the microphone by clicking on the icon 110 | or pressing the shortcut key. 111 | 112 | 113 | how? 114 | ==== 115 | 116 | this extension is available via the official `gnome-shell extensions 117 | repository `_: 118 | 119 | https://extensions.gnome.org/extension/1113/nothing-to-say/ 120 | 121 | alternatively, if you‘re feeling adventurous or want to contribute, 122 | put a clone of this repository (or a symlink) in this directory:: 123 | 124 | ~/.local/share/gnome-shell/extensions/nothing-to-say@extensions.gnome.wouter.bolsterl.ee/ 125 | 126 | note that the files must be directly in this directory, not in a 127 | subdirectory thereof. 128 | 129 | why? 130 | ==== 131 | 132 | when participating in a group call, it is very likely that you are not 133 | speaking most of the time, unless you are the main speaker in a remote 134 | presentation. so why stream all your background noise to the rest of 135 | the attendees? 136 | 137 | think for a bit. oh yes. you have heard ringing phones, crying babies, 138 | coughs, sneezes, or, if you have been particularly unlucky, even less 139 | appetizing sounds. at some point people get annoyed. someone will 140 | speak up to ask others to please be quiet. the original conversation 141 | got interrupted. the attendees got distracted. what were we talking 142 | about again? what was this meeting supposed to be about in the first 143 | place? 144 | 145 | oops, i digress. again. 146 | 147 | luckily most teleconferencing applications allow you to mute yourself. 148 | however, that usually involves clicking a button in that application‘s 149 | window. and that application may not be visible. because you were just 150 | getting some real work done. right? 151 | 152 | nah. more likely, you were looking at cat pictures. oh boy, this one 153 | is seriously cute. oh wow. this one is even cuter. 154 | 155 | at this point someone in the meeting suddenly asks you a question. 156 | 157 | focus. think. act. you have to quickly find the correct window. 158 | dammit, where has that browser tab gone? ah, found it. unmute 159 | yourself. speak for a bit. now mute yourself again. 160 | 161 | so many things to do when you just want to speak a few wise words. 162 | ‘correct, boss, as usual you are completely right!’ 163 | 164 | now. that was stressful. 165 | 166 | situations like that need fixing. that’s why. 167 | 168 | 169 | who wrote this? 170 | =============== 171 | 172 | wouter bolsterlee. wbolster. 173 | 174 | https://github.com/wbolster on github. star my repos. fork them. and so on. 175 | 176 | https://twitter.com/wbolster on twitter. follow me. or say hi. 177 | 178 | 179 | license 180 | ======= 181 | 182 | © 2016–2022 wouter bolsterlee 183 | 184 | licensed under gpl v2. see license file for details. contains code snippets originating from gnome-shell itself, which is also gpl v2. 185 | 186 | sounds from Kenney's Interface Sounds, CC0: 187 | https://www.kenney.nl/assets/interface-sounds 188 | 189 | anything else? 190 | ============== 191 | 192 | oh yes. this is alpha quality experimental software. feedback welcome 193 | via the issue tracker, both praise and complaints. although preferably 194 | the former. 195 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import GLib from "gi://GLib"; 4 | import Gio from "gi://Gio"; 5 | import St from "gi://St"; 6 | import Meta from "gi://Meta"; 7 | import Shell from "gi://Shell"; 8 | import Gvc from "gi://Gvc"; 9 | import GObject from "gi://GObject"; 10 | 11 | import * as Main from "resource:///org/gnome/shell/ui/main.js"; 12 | import * as PanelMenu from "resource:///org/gnome/shell/ui/panelMenu.js"; 13 | import { Extension } from "resource:///org/gnome/shell/extensions/extension.js"; 14 | 15 | import * as Signals from "resource:///org/gnome/shell/misc/signals.js"; 16 | 17 | const EXCLUDED_APPLICATION_IDS = [ 18 | "org.gnome.VolumeControl", 19 | "org.PulseAudio.pavucontrol", 20 | ]; 21 | const KEYBINDING_KEY_NAME = "keybinding-toggle-mute"; 22 | const MICROPHONE_ACTIVE_STYLE_CLASS = "screencast-indicator"; 23 | 24 | let initialised = false; // flag to avoid notifications on startup 25 | let settings = null; 26 | let microphone; 27 | let audio_player; 28 | let panel_button; 29 | 30 | class Microphone extends Signals.EventEmitter { 31 | constructor() { 32 | super(); 33 | this.active = null; 34 | this.stream = null; 35 | this.muted_changed_id = 0; 36 | this.mixer_control = new Gvc.MixerControl({ name: "Nothing to say" }); 37 | this.mixer_control.open(); 38 | const refresh_cb = () => { 39 | this.refresh(); 40 | }; 41 | this.mixer_control.connect("default-source-changed", refresh_cb); 42 | this.mixer_control.connect("stream-added", refresh_cb); 43 | this.mixer_control.connect("stream-removed", refresh_cb); 44 | this.refresh(); 45 | } 46 | 47 | refresh() { 48 | // based on gnome-shell volume control 49 | if (this.stream && this.muted_changed_id) { 50 | this.stream.disconnect(this.muted_changed_id); 51 | } 52 | let was_active = this.active; 53 | this.active = false; 54 | this.stream = this.mixer_control.get_default_source(); 55 | if (this.stream) { 56 | this.muted_changed_id = this.stream.connect("notify::is-muted", () => { 57 | this.notify_muted(); 58 | }); 59 | let recording_apps = this.mixer_control.get_source_outputs(); 60 | for (let i = 0; i < recording_apps.length; i++) { 61 | let output_stream = recording_apps[i]; 62 | let application_id = output_stream.get_application_id(); 63 | if (EXCLUDED_APPLICATION_IDS.includes(application_id)) continue; 64 | this.active = true; 65 | } 66 | } 67 | this.notify_muted(); 68 | if (this.active != was_active) { 69 | this.emit("notify::active"); 70 | } 71 | } 72 | 73 | destroy() { 74 | this.mixer_control.close(); 75 | } 76 | 77 | notify_muted() { 78 | this.emit("notify::muted"); 79 | } 80 | 81 | get muted() { 82 | if (!this.stream) return true; 83 | return this.stream.is_muted; 84 | } 85 | 86 | set muted(muted) { 87 | if (!this.stream) return; 88 | this.stream.change_is_muted(muted); 89 | } 90 | 91 | get level() { 92 | if (!this.stream) return 0; 93 | return this.stream.get_volume() / this.mixer_control.get_vol_max_norm(); 94 | } 95 | } 96 | 97 | class AudioPlayer { 98 | #sound_on; 99 | #sound_off; 100 | 101 | constructor(dir) { 102 | this.#sound_on = dir.get_child("sounds/on.ogg"); 103 | this.#sound_off = dir.get_child("sounds/off.ogg"); 104 | } 105 | 106 | #play_sound(sound_file) { 107 | let player = global.display.get_sound_player(); 108 | player.play_from_file(sound_file, "Toggle mute", null); 109 | } 110 | 111 | play_on() { 112 | this.#play_sound(this.#sound_on); 113 | } 114 | 115 | play_off() { 116 | this.#play_sound(this.#sound_off); 117 | } 118 | } 119 | 120 | const MicrophonePanelButton = GObject.registerClass( 121 | { GTypeName: "MicrophonePanelButton" }, 122 | class extends PanelMenu.Button { 123 | _init(extension) { 124 | super._init(0.0, `${extension.metadata.name} panel indicator`, false); 125 | this.icon = new St.Icon({ 126 | icon_name: get_icon_name(false), 127 | style_class: "system-status-icon", 128 | }); 129 | this.add_child(this.icon); 130 | this.connect("button-press-event", (_, event) => { 131 | if (event.get_button() === 3) { 132 | // Right click. 133 | extension.openPreferences(); 134 | } else { 135 | on_activate({ give_feedback: false }); 136 | } 137 | }); 138 | } 139 | }, 140 | ); 141 | 142 | function get_icon_name(muted) { 143 | // TODO: use -low and -medium icons based on .level 144 | return muted 145 | ? "microphone-sensitivity-muted-symbolic" 146 | : "microphone-sensitivity-high-symbolic"; 147 | } 148 | 149 | function icon_should_be_visible(microphone_active) { 150 | let setting = settings.get_value("icon-visibility").unpack(); 151 | switch (setting) { 152 | case "always": 153 | return true; 154 | case "never": 155 | return false; 156 | default: 157 | return microphone.active; // when-recording 158 | } 159 | } 160 | 161 | function show_osd(text, muted, level) { 162 | const monitor = -1; 163 | const icon = Gio.Icon.new_for_string(get_icon_name(muted)); 164 | Main.osdWindowManager.show(monitor, icon, text, level); 165 | } 166 | 167 | function on_activate({ give_feedback }) { 168 | toggle_mute(!microphone.muted, give_feedback); 169 | } 170 | 171 | let toggle_mute_timeout_id = null; 172 | 173 | function toggle_mute(mute, give_feedback) { 174 | // use a delay before toggling; this makes push-to-talk/mute work 175 | if (toggle_mute_timeout_id) { 176 | GLib.Source.remove(toggle_mute_timeout_id); 177 | if (give_feedback) { 178 | // keep osd visible 179 | show_osd(null, !mute, mute ? microphone.level : 0); 180 | } 181 | } 182 | toggle_mute_timeout_id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 100, () => { 183 | toggle_mute_timeout_id = null; 184 | microphone.muted = mute; 185 | if (give_feedback) { 186 | show_osd(null, mute, mute ? 0 : microphone.level); 187 | } 188 | if (settings.get_boolean("play-feedback-sounds")) { 189 | if (mute) { 190 | audio_player.play_off(); 191 | } else { 192 | audio_player.play_on(); 193 | } 194 | } 195 | }); 196 | } 197 | 198 | export default class extends Extension { 199 | enable() { 200 | settings = this.getSettings(); 201 | microphone = new Microphone(); 202 | audio_player = new AudioPlayer(this.dir); 203 | panel_button = new MicrophonePanelButton(this); 204 | panel_button.visible = icon_should_be_visible(microphone.active); 205 | const indicatorName = `${this.metadata.name} indicator`; 206 | Main.panel.addToStatusArea(indicatorName, panel_button, 0, "right"); 207 | microphone.connect("notify::active", () => { 208 | if (microphone.active) { 209 | panel_button.icon.add_style_class_name(MICROPHONE_ACTIVE_STYLE_CLASS); 210 | } else { 211 | panel_button.icon.remove_style_class_name( 212 | MICROPHONE_ACTIVE_STYLE_CLASS, 213 | ); 214 | } 215 | panel_button.visible = icon_should_be_visible(microphone.active); 216 | if ( 217 | settings.get_boolean("show-osd") && 218 | (initialised || microphone.active) 219 | ) 220 | show_osd( 221 | microphone.active ? "Microphone activated" : "Microphone deactivated", 222 | microphone.muted, 223 | ); 224 | initialised = true; 225 | }); 226 | microphone.connect("notify::muted", () => { 227 | panel_button.icon.icon_name = get_icon_name(microphone.muted); 228 | }); 229 | Main.wm.addKeybinding( 230 | KEYBINDING_KEY_NAME, 231 | settings, 232 | Meta.KeyBindingFlags.NONE, 233 | Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, 234 | () => { 235 | on_activate({ give_feedback: settings.get_boolean("show-osd") }); 236 | }, 237 | ); 238 | settings.connect("changed::icon-visibility", () => { 239 | panel_button.visible = icon_should_be_visible(microphone.active); 240 | }); 241 | } 242 | 243 | disable() { 244 | Main.wm.removeKeybinding(KEYBINDING_KEY_NAME); 245 | Main.panel._rightBox.remove_child(panel_button); 246 | settings = null; 247 | microphone.destroy(); 248 | microphone = null; 249 | panel_button.destroy(); 250 | panel_button = null; 251 | if (toggle_mute_timeout_id) { 252 | GLib.Source.remove(toggle_mute_timeout_id); 253 | toggle_mute_timeout_id = null; 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Unmute the microphone only when you have something to say.", 3 | "name": "Nothing to say", 4 | "settings-schema": "org.gnome.shell.extensions.nothing-to-say", 5 | "shell-version": [ 6 | "48", 7 | "47", 8 | "46", 9 | "45" 10 | ], 11 | "url": "https://github.com/wbolster/nothing-to-say", 12 | "uuid": "nothing-to-say@extensions.gnome.wouter.bolsterl.ee" 13 | } 14 | -------------------------------------------------------------------------------- /notes.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | notes 3 | ===== 4 | 5 | gnome-shell 6 | =========== 7 | 8 | gnome-shell/js/ui/status/volume.js 9 | 10 | libgnome-volume-control 11 | ======================= 12 | 13 | https://github.com/GNOME/libgnome-volume-control/blob/master/gvc-mixer-control.c 14 | 15 | Gvc = imports.gi.Gvc; 16 | https://www.roojs.org/seed/gir-1.2-gtk-3.0/seed/Gvc.MixerControl.html 17 | default_source_changed 18 | 19 | https://www.roojs.org/seed/gir-1.2-gtk-3.0/seed/Gvc.MixerStream.html 20 | 21 | 22 | volume mixer extension 23 | ====================== 24 | 25 | https://github.com/aleho/gnome-shell-volume-mixer/blob/master/shell-volume-mixer%40derhofbauer.at/mixer.js 26 | hotkeys: https://github.com/aleho/gnome-shell-volume-mixer/blob/master/shell-volume-mixer%40derhofbauer.at/hotkeys.js 27 | 28 | 29 | audio switcher extension 30 | ======================== 31 | 32 | https://extensions.gnome.org/extension/1028/gnome-shell-audio-output-switcher/ 33 | https://github.com/AndresCidoncha/audio-switcher/blob/master/extension.js 34 | -------------------------------------------------------------------------------- /prefs.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Gio from "gi://Gio"; 4 | import Gtk from "gi://Gtk"; 5 | import GObject from "gi://GObject"; 6 | import Adw from "gi://Adw"; 7 | 8 | import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js"; 9 | 10 | const KeyValuePair = GObject.registerClass( 11 | { 12 | Properties: { 13 | key: GObject.ParamSpec.string( 14 | "key", 15 | null, 16 | null, 17 | GObject.ParamFlags.READWRITE, 18 | "", 19 | ), 20 | value: GObject.ParamSpec.string( 21 | "value", 22 | "Value", 23 | "Value", 24 | GObject.ParamFlags.READWRITE, 25 | "", 26 | ), 27 | }, 28 | }, 29 | class KeyValuePair extends GObject.Object {}, 30 | ); 31 | 32 | export default class extends ExtensionPreferences { 33 | fillPreferencesWindow(window) { 34 | const settings = this.getSettings(); 35 | 36 | const page = new Adw.PreferencesPage({ 37 | icon_name: "dialog-information-symbolic", 38 | }); 39 | window.add(page); 40 | 41 | let group = new Adw.PreferencesGroup({ 42 | title: "Keybindings", 43 | description: "Keybindings for muting and unmuting", 44 | }); 45 | page.add(group); 46 | 47 | // keybinding row 48 | group.add( 49 | (() => { 50 | const accel = 51 | settings.get_strv("keybinding-toggle-mute")[0] || "backslash"; 52 | const keybindingsRow = new Adw.EntryRow({ 53 | title: "Mute/Unmute", 54 | show_apply_button: true, 55 | text: accel, 56 | }); 57 | const resetButton = new Gtk.Button({ 58 | icon_name: "edit-clear-symbolic", 59 | tooltip_text: "Reset to default", 60 | valign: Gtk.Align.CENTER, 61 | }); 62 | resetButton.connect("clicked", () => { 63 | settings.reset("keybinding-toggle-mute"); 64 | keybindingsRow.text = 65 | settings.get_strv("keybinding-toggle-mute")[0] || "backslash"; 66 | }); 67 | keybindingsRow.add_suffix(resetButton); 68 | keybindingsRow.connect("apply", () => { 69 | settings.set_strv("keybinding-toggle-mute", [keybindingsRow.text]); 70 | }); 71 | return keybindingsRow; 72 | })(), 73 | ); 74 | 75 | group = new Adw.PreferencesGroup({ 76 | title: "Other", 77 | }); 78 | page.add(group); 79 | 80 | // top bar icon row 81 | group.add( 82 | (() => { 83 | const model = new Gio.ListStore({ item_type: KeyValuePair }); 84 | model.splice(0, 0, [ 85 | new KeyValuePair({ key: "when-recording", value: "When recording" }), 86 | new KeyValuePair({ key: "always", value: "Always" }), 87 | new KeyValuePair({ key: "never", value: "Never" }), 88 | ]); 89 | const iconVisibleComboBox = new Adw.ComboRow({ 90 | title: "Top bar icon", 91 | subtitle: "Whether to show top bar icon", 92 | model: model, 93 | expression: new Gtk.PropertyExpression(KeyValuePair, null, "value"), 94 | }); 95 | for (let i = 0; i < model.n_items; i++) { 96 | if ( 97 | model.get_item(i).key === 98 | settings.get_string("icon-visibility", "when-recording") 99 | ) { 100 | iconVisibleComboBox.selected = i; 101 | break; 102 | } 103 | } 104 | iconVisibleComboBox.connect("notify::selected-item", () => { 105 | const selected_item = iconVisibleComboBox.selected_item; 106 | if (selected_item) { 107 | settings.set_string("icon-visibility", selected_item.key); 108 | } 109 | }); 110 | settings.bind( 111 | "icon-visibility", 112 | iconVisibleComboBox, 113 | "active-id", 114 | Gio.SettingsBindFlags.DEFAULT, 115 | ); 116 | return iconVisibleComboBox; 117 | })(), 118 | ); 119 | 120 | // osd row 121 | const toggleOSD = new Adw.SwitchRow({ 122 | title: "OSD notification", 123 | subtitle: "Whether to show OSD notification", 124 | }); 125 | settings.bind( 126 | "show-osd", 127 | toggleOSD, 128 | "active", 129 | Gio.SettingsBindFlags.DEFAULT, 130 | ); 131 | group.add(toggleOSD); 132 | 133 | // sound notification row 134 | const feedbackSoundsSwitch = new Adw.SwitchRow({ 135 | title: "Sound notification", 136 | subtitle: "Play sound when muting and unmuting", 137 | }); 138 | settings.bind( 139 | "play-feedback-sounds", 140 | feedbackSoundsSwitch, 141 | "active", 142 | Gio.SettingsBindFlags.DEFAULT, 143 | ); 144 | group.add(feedbackSoundsSwitch); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /schemas/gschemas.compiled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wbolster/nothing-to-say/0d448688a4e6a40af7ac81e0ccd5812cff528a20/schemas/gschemas.compiled -------------------------------------------------------------------------------- /schemas/org.gnome.shell.extensions.nothing-to-say.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 'when-recording' 11 | When to show the icon 12 | This specifies when the microphone icon in the topbar is visible. 13 | 14 | 15 | ["<Super>backslash"] 16 | Mute or unmute 17 | These shortcut keys will mute or unmute the microphone when pressed once, and will act as a push-to-talk button when held down. 18 | 19 | 20 | true 21 | Whether to show OSD notification 22 | This specifies whether to show OSD notification. 23 | 24 | 25 | true 26 | Play sound when muting and unmuting 27 | Play sounds when the microphone is muted and unmuted. 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /screenshot-osd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wbolster/nothing-to-say/0d448688a4e6a40af7ac81e0ccd5812cff528a20/screenshot-osd.png -------------------------------------------------------------------------------- /screenshot-top-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wbolster/nothing-to-say/0d448688a4e6a40af7ac81e0ccd5812cff528a20/screenshot-top-bar.png -------------------------------------------------------------------------------- /sounds/off.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wbolster/nothing-to-say/0d448688a4e6a40af7ac81e0ccd5812cff528a20/sounds/off.ogg -------------------------------------------------------------------------------- /sounds/on.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wbolster/nothing-to-say/0d448688a4e6a40af7ac81e0ccd5812cff528a20/sounds/on.ogg --------------------------------------------------------------------------------