Gnome Shell Extension Key Binding - linux

What is the simplest way to (globally) bind a key combination (e.g. <Super>+A) to a function in a gnome shell extension?
Inspecting a couple of extensions, I ran into the following code:
global.display.add_keybinding('random-name',
new Gio.Settings({schema: 'org.gnome.shell.keybindings'}),
Meta.KeyBindingFlags.NONE,
function() { /* ... some code */ });
I understand that the key combination is specified by the schema parameter, and that it's possible to create an XML file describing the combination. Is there a simpler way to do this?

The question is old, but I just implemented that for Gnome Shell 40. So here is how I did it.
The key is defined in your normal schema file that you use for the settings of the extension. So it looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="org.gnome.shell.extensions.mycoolstuff" path="/org/gnome/shell/extensions/mycoolstuff/">
<key name="cool-hotkey" type="as">
<default><![CDATA[['<Ctrl><Super>T']]]></default>
<summary>Hotkey to open the cool stuff.</summary>
</key>
... other config options
</schema>
</schemalist>
The key type is a "Array of String", so you can configure multiple key-combinations for the action.
In your code you use it like this:
const Main = imports.ui.main;
const Meta = imports.gi.Meta
const Shell = imports.gi.Shell
const ExtensionUtils = imports.misc.extensionUtils;
...
let my_settings = ExtensionUtils.getSettings("org.gnome.shell.extensions.mycoolstuff");
Main.wm.addKeybinding("cool-hotkey", my_settings,
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW
this._hotkeyActionMethod.bind(this));
I would recommend to remove the key binding when the extension gets disabled.
Don't know what happens if you don't do this.
Main.wm.removeKeybinding("cool-hotkey");
BTW: Changes to the settings (via dconf editor, gsettings or your extensions preferences) are active immediately.

Following is a copy of my answer here
I've only tested this in Gnome 3.22
TL;DR
Here is a class:
KeyManager: new Lang.Class({
Name: 'MyKeyManager',
_init: function() {
this.grabbers = new Map()
global.display.connect(
'accelerator-activated',
Lang.bind(this, function(display, action, deviceId, timestamp){
log('Accelerator Activated: [display={}, action={}, deviceId={}, timestamp={}]',
display, action, deviceId, timestamp)
this._onAccelerator(action)
}))
},
listenFor: function(accelerator, callback){
log('Trying to listen for hot key [accelerator={}]', accelerator)
let action = global.display.grab_accelerator(accelerator)
if(action == Meta.KeyBindingAction.NONE) {
log('Unable to grab accelerator [binding={}]', accelerator)
} else {
log('Grabbed accelerator [action={}]', action)
let name = Meta.external_binding_name_for_action(action)
log('Received binding name for action [name={}, action={}]',
name, action)
log('Requesting WM to allow binding [name={}]', name)
Main.wm.allowKeybinding(name, Shell.ActionMode.ALL)
this.grabbers.set(action, {
name: name,
accelerator: accelerator,
callback: callback
})
}
},
_onAccelerator: function(action) {
let grabber = this.grabbers.get(action)
if(grabber) {
this.grabbers.get(action).callback()
} else {
log('No listeners [action={}]', action)
}
}
})
And that's how you you use it:
let keyManager = new KeyManager()
keyManager.listenFor("<ctrl><shift>a", function(){
log("Hot keys are working!!!")
})
You're going to need imports:
const Lang = imports.lang
const Meta = imports.gi.Meta
const Shell = imports.gi.Shell
const Main = imports.ui.main
Explanation
I might be terribly wrong, but that what I've figured out in last couple days.
First of all it is Mutter who is responsible for listening for hotkeys. Mutter is a framework for creating Window Managers, it is not an window manager itself.
Gnome Shell has a class written in JS and called "Window Manager" - this is the real Window Manager which uses Mutter internally to do all low-level stuff.
Mutter has an object MetaDisplay. This is object you use to request listening for a hotkey.
But!
But Mutter will require Window Manager to approve usage of this hotkey. So what happens when hotkey is pressed?
- MetaDisplay generates event 'filter-keybinding'.
- Window Manager in Gnome Shell checks if this hotkey allowed to be processed.
- Window Manager returns appropriate value to MetaDisplay
- If it is allowed to process this hotkey, MetaDisplay generates event 'accelerator-actived'
- Your extension must listen for that event and figure out by action id which hotkey is activated.

Same as that of #p2t2p but recast using ES5 class. This is also using my logger class but you can replace that with log().
const Lang = imports.lang
const Meta = imports.gi.Meta
const Shell = imports.gi.Shell
const Main = imports.ui.main
const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Logger = Me.imports.logger.Logger;
var KeyboardShortcuts = class KeyboardShortcuts {
constructor(settings) {
this._grabbers = {};
this.logger = new Logger('kt kbshortcuts', settings);
global.display.connect('accelerator-activated', (display, action, deviceId, timestamp) => {
this.logger.debug("Accelerator Activated: [display=%s, action=%s, deviceId=%s, timestamp=%s]",
display, action, deviceId, timestamp)
this._onAccelerator(action)
});
}
listenFor(accelerator, callback) {
this.logger.debug('Trying to listen for hot key [accelerator=%s]', accelerator);
let action = global.display.grab_accelerator(accelerator, 0);
if (action == Meta.KeyBindingAction.NONE) {
this.logger.error('Unable to grab accelerator [%s]', accelerator);
return;
}
this.logger.debug('Grabbed accelerator [action={}]', action);
let name = Meta.external_binding_name_for_action(action);
this.logger.debug('Received binding name for action [name=%s, action=%s]',
name, action)
this.logger.debug('Requesting WM to allow binding [name=%s]', name)
Main.wm.allowKeybinding(name, Shell.ActionMode.ALL)
this._grabbers[action]={
name: name,
accelerator: accelerator,
callback: callback
};
}
_onAccelerator(action) {
let grabber = this._grabbers[action];
if (grabber) {
grabber.callback();
} else {
this.logger.debug('No listeners [action=%s]', action);
}
}
}
and use it like,
this.accel = new KeyboardShortcuts(this.settings);
this.accel.listenFor("<ctrl><super>T", () => {
this.logger.debug("Toggling show endtime");
this._timers.settings.show_endtime = !this._timers.settings.show_endtime;
});

Related

How to mock document.currentScript to return a valid script object with data attributes

I'm trying to write some tests in jest for an API that is intended to be shared with third parties. The inclusion method used by the third parties is a script tag which initiates the script but also passes along some data attributes via document.currentScript.
Here is a very cut down version of the code with the relevant parts:
// clientLib.js
export const getCurrentScript = () => document.currentScript;
export const initLib = () => {
const currentScript = getCurrentScript();
const reference = currentScript.getAttribute('data-reference');
const lang = currentScript.getAttribute('data-lang');
return new clientLib(reference, lang);
}
export class clientLib {
constructor(reference, lang) {
this._brand = reference;
this._market = lang;
}
}
window.clientLib = initLib();
// html
<script src="clientLib.js" data-reference="12345" data-lang="en-GB"></script>
What I'd like to be able to do in my test is something like this, but I've been unable to get anything to work:
// clientLib.test.js
import {
getCurrentScript,
initLib,
clientLib
} from './clientLib';
// here I want to mock the output of getCurrentScript() since document.currentScript does
// not exist, and I need the mock to return a script object with the two data attributes
// (ideally I need to be able to recreate this mock with both, either or none of the attributes)
// to test other cases
describe('initLib', () => {
it('returns a new instance of the library based on script attributes', () => {
window.clientLib = initLib();
// in here I should then be able to access properties on my lib on the window object
});
});
I did manage to get this working using an external setup file but for logistical reasons I don't think I can use this approach. I've tried to mock it in the test file itself and keep coming back to an "Invalid variable access: document" error. Is there a way to do this that I'm missing?

how can i use the telegraf-inline-menu module in my code

Hi I have a code like this:
const Telegraf = require('telegraf');
const TelegrafInlineMenu = require('telegraf-inline-menu');
const bot = new Telegraf(process.env.BOT_TOKEN);
const menu = new TelegrafInlineMenu('menu');
menu.setCommand('list');
menu.simpleButton('I am excited', 'a', {
doFunc: ctx => ctx.reply('as I am')
});
bot.on('text', (ctx) => {
if(ctx.message.text == 'list') {
// i want to run my inline menu here
}
});
bot.startPolling();
Actually I want to show a text with a inline menu when the user send 'list' command.
How can I run the inline menu where the comment is?
I write bot.use(menu.init()) but it didn't work.
Thanks for your help!
Did you forgot to launch bot?
bot.use(menu.init());
bot.launch();

Electron: get full path of uploaded file

I'm buildind now GUI using Electron. (like PhoneGap for desktop apps)
Is there a way to enable full path for file checked in <input type="file">?
Insted of C:\fakepath\dataset.zip now. (the directory name isn't "fakepath", but that is the value of document.getElementById("myFile").value)
Or, is there other way to select a file?
Electron adds a path property to File objects, so you can get the real path from the input element using:
document.getElementById("myFile").files[0].path
<script>
const electron = require('electron');
const { ipcRenderer } = electron;
const ko = require('knockout')
const fs = require('fs');
const request = require('request-promise');
// replace with your own paths
var zipFilePath = 'C:/Users/malco/AppData/Roaming/Wimpsdata/Wimpsdata.zip';
var uploadUri = 'http://localhost:59887/api/Collector/Upload'
var request = require('request');
request.post({
headers: { 'content-type': 'application/zip' },
url: uploadUri,
body: fs.createReadStream(zipFilePath)
}, function (error, response, body) {
console.log(body);
location.href = 'ScanResults.html';
});
</script>
ASP .NET WebAPI Conontroller
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using Wimps.Services.Business;
namespace Wimps.Services.Controllers
{
public class CollectorController : ApiController
{
public async Task<bool> Upload()
{
try
{
var fileuploadPath = ConfigurationManager.AppSettings["FileUploadLocation"];
var provider = new MultipartFormDataStreamProvider(fileuploadPath);
var content = new StreamContent(HttpContext.Current.Request.GetBufferlessInputStream(true));
foreach (var header in Request.Content.Headers)
{
content.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
Byte[] byteArray = await content.ReadAsByteArrayAsync();
string newFileName = Guid.NewGuid().ToString();
string newFilePath = fileuploadPath + "\\" + newFileName + ".zip";
if (File.Exists(newFilePath))
{
File.Delete(newFilePath);
}
File.WriteAllBytes(newFilePath, byteArray);
string unzipTo = fileuploadPath + "\\" + newFileName;
Directory.CreateDirectory(unzipTo);
DirectoryInfo di = new DirectoryInfo(unzipTo);
foreach (FileInfo file in di.GetFiles())
{
file.Delete();
}
ZipFile.ExtractToDirectory(newFilePath, unzipTo);
return true;
}
catch (Exception e)
{
// handle exception here
return false;
}
}
}
}
Need to add key to web config for file upload
<configuration>
<appSettings>
... other keys here
<add key="FileUploadLocation" value="C:\Temp\Uploads" />
</appSettings>
rest of app config
...
...
It is not possible to do what you are trying for security reasons, according this answer How to get full path of selected file on change of <input type=‘file’> using javascript, jquery-ajax?.
However you could do a work around like I did in an electron project I worked on.
Create a HTML button
Then in the renderer process create an event listener to the button you created before.
const ipc = require('electron').ipcRenderer;
const buttonCreated = document.getElementById('button-created-id');
buttonCreated.addEventListener('click', function (event) {
ipc.send('open-file-dialog-for-file')
});
Then in the main process you use the showOpenDialog to choose a file and then send the full path back to the renderer process.
ipc.on('open-file-dialog-for-file', function (event) {
if(os.platform() === 'linux' || os.platform() === 'win32'){
dialog.showOpenDialog({
properties: ['openFile']
}, function (files) {
if (files) event.sender.send('selected-file', files[0]);
});
} else {
dialog.showOpenDialog({
properties: ['openFile', 'openDirectory']
}, function (files) {
if (files) event.sender.send('selected-file', files[0]);
});
}});
Then in the renderer process you get the full path.
ipc.on('selected-file', function (event, path) {
console.log('Full path: ', path);
});
Thus you can have a similar behaviour than the input type file and get the full path.
The accepted answer works great for the original question, but the answer from #Piero-Divasto works a lot better for my purposes.
What I needed was the pathname of a directory which may be rather large. Using the accepted answer, this can block the main process for several seconds while it processes the directory contents. Using dialog.showOpenDialog(...) gets me a near-instant response. The only difference is that dialog.showOpenDialog doesn't take a callback function anymore, and instead returns a promise:
ipcMain.on("open-file-dialog-for-dir", async event => {
const dir = await dialog.showOpenDialog({ properties: ["openDirectory"] });
if (dir) {
event.sender.send("selected-dir", dir.filePaths[0]);
}
});
<script>const electron = require('electron');</script>
<button id="myFile" onclick="this.value=electron.remote.dialog.showOpenDialog()[0]">UpdateFile</button>
Now, the document.getElementById("myFile").value would contain the full path of the chosen file.
As answered by Vadim Macagon:
let { path } = document.getElementById("myFile").files[0]
Since there is no included interface for this for TypeScript as of this answer, to use this you have to cast the File to another type
let { path } = document.getElementById("myFile").files[0] as any
or, if you would rather not use any
interface ElectronFile extends File {
path: string;
}
let { path } = document.getElementById("myFile").files[0] as ElectronFile

How can I force external links from browser-window to open in a default browser from Electron?

I'm using the BrowserWindow to display an app and I would like to force the external links to be opened in the default browser. Is that even possible or I have to approach this differently?
I came up with this, after checking the solution from the previous answer.
mainWindow.webContents.on('new-window', function(e, url) {
e.preventDefault();
require('electron').shell.openExternal(url);
});
According to the electron spec, new-window is fired when external links are clicked.
NOTE: Requires that you use target="_blank" on your anchor tags.
new-window is now deprecated in favor of setWindowOpenHandler in Electron 12 (see https://github.com/electron/electron/pull/24517).
So a more up to date answer would be:
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
Improved from the accepted answer ;
the link must be target="_blank" ;
add in background.js(or anywhere you created your window) :
window.webContents.on('new-window', function(e, url) {
// make sure local urls stay in electron perimeter
if('file://' === url.substr(0, 'file://'.length)) {
return;
}
// and open every other protocols on the browser
e.preventDefault();
shell.openExternal(url);
});
Note : To ensure this behavior across all application windows, this code should be run after each window creation.
If you're not using target="_blank" in your anchor elements, this might work for you:
const shell = require('electron').shell;
$(document).on('click', 'a[href^="http"]', function(event) {
event.preventDefault();
shell.openExternal(this.href);
});
I haven't tested this but I assume this is should work:
1) Get WebContents of the your BrowserWindow
var wc = browserWindow.webContents;
2) Register for will-navigate of WebContent and intercept navigation/link clicks:
wc.on('will-navigate', function(e, url) {
/* If url isn't the actual page */
if(url != wc.getURL()) {
e.preventDefault();
openBrowser(url);
}
}
3) Implement openBrowser using child_process. An example for Linux desktops:
var openBrowser(url) {
require('child_process').exec('xdg-open ' + url);
}
let me know if this works for you!
For anybody coming by.
My use case:
I was using SimpleMDE in my app and it's preview mode was opening links in the same window. I wanted all links to open in the default OS browser. I put this snippet, based on the other answers, inside my main.js file. It calls it after it creates the new BrowserWindow instance. My instance is called mainWindow
let wc = mainWindow.webContents
wc.on('will-navigate', function (e, url) {
if (url != wc.getURL()) {
e.preventDefault()
electron.shell.openExternal(url)
}
})
Check whether the requested url is an external link. If yes then use shell.openExternal.
mainWindow.webContents.on('will-navigate', function(e, reqUrl) {
let getHost = url=>require('url').parse(url).host;
let reqHost = getHost(reqUrl);
let isExternal = reqHost && reqHost != getHost(wc.getURL());
if(isExternal) {
e.preventDefault();
electron.shell.openExternal(reqUrl);
}
}
Put this in renderer side js file. It'll open http, https links in user's default browser.
No JQuery attached! no target="_blank" required!
let shell = require('electron').shell
document.addEventListener('click', function (event) {
if (event.target.tagName === 'A' && event.target.href.startsWith('http')) {
event.preventDefault()
shell.openExternal(event.target.href)
}
})
For Electron 5, this is what worked for me:
In main.js (where you create your browser window), include 'shell' in your main require statement (usually at the top of the file), e.g.:
// Modules to control application life and create native browser window
const {
BrowserWindow,
shell
} = require('electron');
Inside the createWindow() function, after mainWindow = new BrowserWindow({ ... }), add these lines:
mainWindow.webContents.on('new-window', function(e, url) {
e.preventDefault();
shell.openExternal(url);
});
I solved the problem by the following step
Add shell on const {app, BrowserWindow} = require('electron')
const {app, BrowserWindow, shell} = require('electron')
Set nativeWindowOpen is true
function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1350,
height: 880,
webPreferences: {
nativeWindowOpen: true,
preload: path.join(__dirname, 'preload.js')
},
icon: path.join(__dirname, './img/icon.icns')
})
Add the following listener code
mainWindow.webContents.on('will-navigate', function(e, reqUrl) {
let getHost = url=>require('url').parse(url).host;
let reqHost = getHost(reqUrl);
let isExternal = reqHost && reqHost !== getHost(wc.getURL());
if(isExternal) {
e.preventDefault();
shell.openExternal(reqUrl, {});
}
})
reference https://stackoverflow.com/a/42570770/7458156 by cuixiping
I tend to use these lines in external .js script:
let ele = document.createElement("a");
let url = "https://google.com";
ele.setAttribute("href", url);
ele.setAttribute("onclick", "require('electron').shell.openExternal('" + url + "')");

What is the best way to expose methods from Node.js?

Consider I want to expose a method called Print
Binding method as prototype:
File Saved as Printer.js
var printerObj = function(isPrinted) {
this.printed = isPrinted;
}
printerObj.prototype.printNow = function(printData) {
console.log('= Print Started =');
};
module.exports = printerObj;
Then access printNow() by putting code require('Printer.js').printNow() in any external .js node program file.
Export method itself using module.exports:
File Saved as Printer2.js
var printed = false;
function printNow() {
console.log('= Print Started =');
}
module.exports.printNow = printNow;
Then access printNow() by putting code require('Printer2.js').printNow() in any external .js node program file.
Can anyone tell what is the difference and best way of doing it with respect to Node.js?
Definitely the first way. It is called the substack pattern and you can read about it on Twitter and on Mikeal Rogers' blog. Some code examples can be found at the jade github repo in the parser:
var Parser = exports = module.exports = function Parser(str, filename, options){
this.input = str;
this.lexer = new Lexer(str, options);
...
};
Parser.prototype = {
context: function(parser){
if (parser) {
this.contexts.push(parser);
} else {
return this.contexts.pop();
}
},
advance: function(){
return this.lexer.advance();
}
};
In the first example you are creating a class, ideally you should use it with "new" in your caller program:
var PrinterObj = require('Printer.js').PrinterObj;
var printer = new PrinterObj();
printer.PrintNow();
This is a good read on the subject: http://www.2ality.com/2012/01/js-inheritance-by-example.html
In the second example you are returning a function.
The difference is that you can have multiple instances of the first example (provided you use new as indicated) but only one instance of the second approach.

Resources