Skip to content

Commit

Permalink
Merge pull request #12 from Woomymy/master
Browse files Browse the repository at this point in the history
Add password-less changes support, localisation support and other changes
  • Loading branch information
AzzamAlsharafi authored Aug 11, 2023
2 parents 4de0e89 + 81f6984 commit c67de82
Show file tree
Hide file tree
Showing 20 changed files with 774 additions and 72 deletions.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Extension built ZIP
*.zip

# Compliled GSChema
gschemas.compiled

# L10N
*.mo
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "subprojects/blueprint-compiler"]
path = subprojects/blueprint-compiler
url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git
8 changes: 8 additions & 0 deletions 99-ideapad.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Required by ideapad_controls gnome extension
z /sys/bus/platform/drivers/ideapad_acpi/*/conservation_mode 0664 root ideapad_laptop
z /sys/bus/platform/drivers/ideapad_acpi/*/camera_power 0664 root ideapad_laptop
Z /sys/bus/platform/drivers/ideapad_acpi/*/fan_mode 0664 root ideapad_laptop
z /sys/bus/platform/drivers/ideapad_acpi/*/fn_lock 0664 root ideapad_laptop
z /sys/bus/platform/drivers/ideapad_acpi/*/touchpad 0664 root ideapad_laptop
z /sys/bus/platform/drivers/ideapad_acpi/*/usb_charging 0664 root ideapad_laptop

45 changes: 45 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
EXTENSION_ZIP := ideapad-controls@azzamalsharafi.gmail.com.shell-extension.zip
TMPFILES_CONF := 99-ideapad.conf

subprojects:
@git submodule update --resursive

template.ui: subprojects
@./subprojects/blueprint-compiler/blueprint-compiler.py compile template.blp > template.ui

$(EXTENSION_ZIP): template.ui
@gnome-extensions pack ./ \
--extra-source=template.ui \
--extra-source=optionsUtils.js \
--extra-source=aggregateMenu.js \
--extra-source=quickSettingsMenu.js \
--extra-source=common.js \
--extra-source=icons/ \
--extra-source=LICENSE.md \
--podir=po \
--force

translations:
@xgettext \
--files-from=po/POTFILES \
--output=po/ideapat-controls.pot \
--from-code=UTF-8 \
--add-comments \
--keyword=_ \
--keyword=C_:1c,2

install: $(EXTENSION_ZIP)
@echo "Installing extension"
gnome-extensions install --force $(EXTENSION_ZIP)
@echo "Extension installed"

tmpfiles-install: $(TMPFILES_CONF)
@echo "Installing tmpfiles.d configuration"
cp -v $(TMPFILES_CONF) /etc/tmpfiles.d/$(TMPFILES_CONF)
@echo "Installed tmpfiles.d configuration. Reboot or run 'systemd-tmpfiles --create' to make it effective"

clean:
rm -f $(EXTENSION_ZIP) template.ui

all: install

22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# IdeaPad Controls

GNOME Shell extension for controling Lenovo IdeaPad laptops options.

**Available options:** Conservation Mode, Camera Lock, Fn Lock, Touchpad Lock, USB charging.
Expand All @@ -19,8 +20,27 @@ GNOME Shell extension for controling Lenovo IdeaPad laptops options.

# Installation

Make sure the `gettext` package is installed or install it with your package manager

## GNOME Extensions
Install from [GNOME Extensions](https://extensions.gnome.org/extension/5260/ideapad-controls/).

## Manual
Clone the repo then execute `install.sh` script.

First, install `make` using your package manager

Clone the repo then execute `make install`


# Password-less setup

By default, root permissions are needed to write sysfs nodes used to control ideapad laptop parameters and this extensions calls `pkexec` which asks for your password to write those files. If you want to make it work without a password, you can follow these instructions.

First, create a group named `ideapad_laptop` and add yourself into that group:
```bash
groupadd ideapad_laptop
usermod -aG ideapad_laptop $USER
```

Then run `sudo make tmpfiles-install`

8 changes: 7 additions & 1 deletion aggregateMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ const Gio = imports.gi.Gio;
const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu;
const Main = imports.ui.main;
const Gettext = imports.gettext;


const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Common = Me.imports.common;

const Domain = Gettext.domain(Me.metadata.uuid);
const { gettext, ngettext } = Domain;
const _ = gettext;

const AggregateMenu = Main.panel.statusArea.aggregateMenu;

var SystemMenu = GObject.registerClass(
Expand All @@ -34,4 +40,4 @@ var SystemMenu = GObject.registerClass(
super.destroy();
}
}
);
);
11 changes: 8 additions & 3 deletions common.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
const Gio = imports.gi.Gio;
const PopupMenu = imports.ui.popupMenu;
const Gettext = imports.gettext;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const optionsUtils = Me.imports.optionsUtils;

const Domain = Gettext.domain(Me.metadata.uuid);
const { gettext, ngettext } = Domain;
const _ = gettext;

function getIcon() {
return Gio.icon_new_for_string(Me.dir.get_path() + "/icons/controls-big-symbolic.svg");
}

function addOptionsToMenu(menu) {
const settings = ExtensionUtils.getSettings("org.gnome.shell.extensions.ideapad-controls");
const settings = ExtensionUtils.getSettings();

const options = optionsUtils.getOptions();
const translatedOptions = optionsUtils.getTranslatedOptions();

// Create a switch item for each option
for (let i = 0; i < options.length; i++) {
// Convert option title to schema key, i.e. "Camera Lock" becomes "camera-lock-option"
const optionKey = options[i].toLowerCase().replace(" ", "-") + "-option";

const optionSwitch = new PopupMenu.PopupSwitchMenuItem(options[i], optionsUtils.getOptionValue(i) === "1");
const optionSwitch = new PopupMenu.PopupSwitchMenuItem(translatedOptions[i], optionsUtils.getOptionValue(i) === "1");
menu.addMenuItem(optionSwitch);

settings.bind(
Expand All @@ -39,7 +44,7 @@ function addOptionsToMenu(menu) {
// Setting button
menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());

const settingsButton = new PopupMenu.PopupMenuItem("Extension Settings");
const settingsButton = new PopupMenu.PopupMenuItem(_("Extension Settings"));

settingsButton.connect("activate", () => ExtensionUtils.openPrefs());

Expand Down
9 changes: 6 additions & 3 deletions extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ const Common = Me.imports.common;
// and GNOME 43 uses QuickSettings (QSystemMenu).
let {SystemMenu} = shellVersion < 43 ? Me.imports.aggregateMenu : Me.imports.quickSettingsMenu;

function init() {}
function init() {
// Initialise gettext
ExtensionUtils.initTranslations(Me.metadata.uuid);
}

let extensionIcon = null;
let extensionMenu = null;
Expand All @@ -32,7 +35,7 @@ let trayListener = null;
function enable() {
extensionIcon = Common.getIcon();

settings = ExtensionUtils.getSettings("org.gnome.shell.extensions.ideapad-controls");
settings = ExtensionUtils.getSettings();

updateLocation(settings);

Expand Down Expand Up @@ -88,4 +91,4 @@ const TrayMenu = GObject.registerClass(
Common.addOptionsToMenu(this.menu);
}
}
);
);
20 changes: 0 additions & 20 deletions install.sh

This file was deleted.

2 changes: 1 addition & 1 deletion metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"42", "43", "44"
],
"version": 10,
"settings-schema": "org.gnome.shell.extensions.ideapad-Controls"
"settings-schema": "org.gnome.shell.extensions.ideapad-controls"
}
88 changes: 79 additions & 9 deletions optionsUtils.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,71 @@
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Gettext = imports.gettext;
const ExtensionUtils = imports.misc.extensionUtils;

const Me = ExtensionUtils.getCurrentExtension();

const Domain = Gettext.domain(Me.metadata.uuid);
const { gettext, ngettext } = Domain;
const _ = gettext;

// Driver path
const filesDir = "/sys/bus/platform/drivers/ideapad_acpi/VPC2004:00/";
let filesSysfsDir = null;

// All options that the extension can support, regardless of the current device running the extension.
const allOptions = ["Conservation Mode", "Camera", "Fn Lock", "Touchpad", "USB Charging"];
const allOptions = [_("Conservation Mode"), _("Camera"), _("Fn Lock"), _("Touchpad"), _("USB Charging")];
// Files names for each option in the driver files.
const allOptionsFiles = ["conservation_mode", "camera_power", "fn_lock", "touchpad", "usb_charging"];

// Lists to store options supported by this device.
let options = null;
let translatedOptions = null;
let optionsFiles = null;

// Extensions settings
let extensionSettings = null;

async function writeStringToFile(string, filePath) {
const fd = Gio.File.new_for_path(filePath);
const contentBytes = new GLib.Bytes(string);

try {
await new Promise((resolve, reject) => {
// Try to write the file and throw an error if it's not writable
// We don't use the REPLACE_DESTINATION FileCreateFlag, as it would lead to permission errors.
fd.replace_contents_bytes_async(contentBytes, null, false, Gio.FileCreateFlags.NONE, null, (_, res) => {
try {
resolve(fd.replace_contents_finish(res));
} catch (e) {
reject(e);
}
});

});
} catch(e) {
imports.ui.main.notify(_("Ideapad Controls"),
_(`Can't write %s to %s. See journalctl logs for more details`).format(string, filePath));
console.log(`Something went wrong while writing ${string} to ${filePath}: ${e}`);
console.log(`Look at the readme for permission errors fixes.`);
}
}

// Check each option and determine if they are available in this device.
function prepareAvailableOptions() {
options = [];
translatedOptions = [];
optionsFiles = [];

if (!extensionSettings) {
extensionSettings = ExtensionUtils.getSettings();
}

filesSysfsDir = extensionSettings.get_string("sysfs-path")

for (let i = 0; i < allOptionsFiles.length; i++) {
if(GLib.file_test(filesDir + allOptionsFiles[i], GLib.FileTest.EXISTS)){
if(GLib.file_test(filesSysfsDir + allOptionsFiles[i], GLib.FileTest.EXISTS)){
options.push(allOptions[i]);
translatedOptions.push(gettext(allOptions[i]));
optionsFiles.push(allOptionsFiles[i]);
}
}
Expand All @@ -34,6 +79,14 @@ function getOptions() {
return options;
}

// Returns available options in this devices (translated)
function getTranslatedOptions() {
if (translatedOptions == null) {
prepareAvailableOptions();
}
return translatedOptions;
}

// Returns available options files in this device.
function getOptionsFiles() {
if (optionsFiles == null) {
Expand All @@ -44,7 +97,7 @@ function getOptionsFiles() {

// Read option value from driver file.
function getOptionValue(optionIndex) {
const file = Gio.File.new_for_path(filesDir + getOptionsFiles()[optionIndex]);
const file = Gio.File.new_for_path(filesSysfsDir + getOptionsFiles()[optionIndex]);
const [, contents, etag] = file.load_contents(null);

const decoder = new TextDecoder("utf-8");
Expand All @@ -55,15 +108,32 @@ function getOptionValue(optionIndex) {

// Write option value to driver file.
function setOptionValue(optionIndex, value) {
GLib.spawn_command_line_async("pkexec bash -c 'echo " + value + " > " + filesDir + getOptionsFiles()[optionIndex] + "'");
const optionFile = getOptionsFiles()[optionIndex];
const destinationFile = filesSysfsDir + optionFile;
const notificationBody = `${value == true ? _("Enabled") : _("Disabled")} ${getTranslatedOptions()[optionIndex]}`;

if (extensionSettings.get_boolean("use-pkexec")) {
if (extensionSettings.get_boolean("send-success-notifications")) {
GLib.spawn_command_line_async("bash -c \"pkexec bash -c 'echo " + value + " > " + destinationFile + `' && notify-send '${_("Ideapad Controls")}' '${notificationBody}' "`);
} else {
GLib.spawn_command_line_async("pkexec bash -c 'echo " + value + " > " + destinationFile + "'");
}
} else {
console.log("Writing string to file " + value + " " + destinationFile);
writeStringToFile(value.toString(), destinationFile);
if (extensionSettings.get_boolean("send-success-notifications")) {
imports.ui.main.notify(_("Ideapad Controls"), notificationBody);
}
}
}

function destroy(){
if(options != null){
function destroy() {
if(options != null) {
options = null;
}

if(optionsFiles != null){
if(optionsFiles != null) {
optionsFiles = null;
}
}
}

7 changes: 7 additions & 0 deletions po/POTFILES
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
template.blp
aggregateMenu.js
common.js
extension.js
optionsUtils.js
prefs.js
quickSettingsMenu.js
Loading

0 comments on commit c67de82

Please sign in to comment.