Compare commits
No commits in common. "main" and "1.0.1" have entirely different histories.
16 changed files with 72 additions and 342 deletions
17
README.md
17
README.md
|
@ -1,13 +1,16 @@
|
||||||
# Chuwi Minibook tablet mode switch
|
# tablet-mode
|
||||||
|
|
||||||
Allow users to toggle a Chuwi Minibook laptop between laptop and tablet mode.
|
Allow users to toggle a convertible laptop between laptop and tablet mode.
|
||||||
This is based on https://github.com/conqp/tablet-mode.
|
|
||||||
|
|
||||||
## Installation
|
## Configuration
|
||||||
|
|
||||||
Just run `install.sh` on your Debian system and enter the name of you local user account.
|
The keyboard device to be deactivated in tablet mode must be specified in `/etc/tablet-mode.conf`:
|
||||||
Reboot after successful installation.
|
|
||||||
|
|
||||||
|
[Devices]
|
||||||
|
keyboard = /dev/input/by-path/platform-i8042-serio-0-event-kbd
|
||||||
|
touchpad = /dev/input/by-path/platform-i8042-serio-1-event-mouse
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
You can toggle between tablet and laptop mode by running `setsysmode toggle` or use the desktop icon provided with this package.
|
You must be a member of the group `tablet` to toggle between tablet and laptop mode.
|
||||||
|
You can toggle between tablet and laptop mode by running `toggle-tablet-mode` or use the desktop icon provided with this package.
|
||||||
|
|
35
install.sh
35
install.sh
|
@ -1,35 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
GROUP=tablet
|
|
||||||
|
|
||||||
if ! [ $(getent group "$GROUP") ]; then
|
|
||||||
echo "Add tablet group..."
|
|
||||||
/usr/sbin/groupadd tablet
|
|
||||||
fi
|
|
||||||
|
|
||||||
read -p "Enter your local username: " user
|
|
||||||
|
|
||||||
if ! [ $(groupmems -g "$GROUP" -l | grep "$user" ) ]; then
|
|
||||||
echo "Add user to group..."
|
|
||||||
usermod -a -G tablet "$user"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Copy files..."
|
|
||||||
cp -r tabletmode/ /usr/local/lib/python3.11/dist-packages
|
|
||||||
cp tablet-mode.service /etc/systemd/system
|
|
||||||
cp laptop-mode.service /etc/systemd/system
|
|
||||||
cp tablet-mode.json /etc/
|
|
||||||
cp tablet-mode.desktop /home/"$user"/.local/applications
|
|
||||||
cp tablet-mode.sudoers /etc/sudoers.d/tablet-mode
|
|
||||||
cp setsysmode /usr/local/bin
|
|
||||||
chmod +x /usr/local/bin/setsysmode
|
|
||||||
cp sysmoded /usr/local/bin
|
|
||||||
chmod +x /usr/local/bin/sysmoded
|
|
||||||
|
|
||||||
echo "Reload systemd..."
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
echo "Install packages..."
|
|
||||||
apt install evtest -y
|
|
|
@ -1,10 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Configure system for laptop mode
|
|
||||||
Conflicts=tablet-mode.service
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
ExecStart=/usr/local/bin/sysmoded laptop
|
|
||||||
StandardOutput=null
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
|
@ -1,8 +0,0 @@
|
||||||
#! /usr/bin/env python3
|
|
||||||
"""Sets the system mode."""
|
|
||||||
|
|
||||||
from tabletmode.cli import main
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
25
setup.py
25
setup.py
|
@ -1,25 +0,0 @@
|
||||||
#! /usr/bin/env python
|
|
||||||
|
|
||||||
from setuptools import setup
|
|
||||||
|
|
||||||
setup(
|
|
||||||
name='tabletmode',
|
|
||||||
use_scm_version=True,
|
|
||||||
setup_requires=['setuptools_scm'],
|
|
||||||
author='Richard Neumann',
|
|
||||||
author_email='mail@richard-neumann.de',
|
|
||||||
python_requires='>=3.8',
|
|
||||||
packages=['tabletmode'],
|
|
||||||
entry_points={
|
|
||||||
'console_scripts': [
|
|
||||||
'setsysmode = tabletmode.cli:main',
|
|
||||||
'sysmoded = tabletmode.daemon:main'
|
|
||||||
],
|
|
||||||
},
|
|
||||||
url='https://github.com/conqp/tablet-mode',
|
|
||||||
license='GPLv3',
|
|
||||||
description='Tablet mode switch for GNOME 3.',
|
|
||||||
long_description=open('README.md').read(),
|
|
||||||
long_description_content_type="text/markdown",
|
|
||||||
keywords='tablet mode tent convertible switch'
|
|
||||||
)
|
|
8
sysmoded
8
sysmoded
|
@ -1,8 +0,0 @@
|
||||||
#! /usr/bin/env python3
|
|
||||||
"""System mode daemon."""
|
|
||||||
|
|
||||||
from tabletmode.daemon import main
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
48
tablet-mode
Executable file
48
tablet-mode
Executable file
|
@ -0,0 +1,48 @@
|
||||||
|
#! /usr/bin/env python3
|
||||||
|
"""Grabs the respective device to discard any input from it."""
|
||||||
|
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from contextlib import suppress
|
||||||
|
from subprocess import run
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_FILE = '/etc/tablet-mode.conf'
|
||||||
|
EVTEST = '/usr/bin/evtest'
|
||||||
|
|
||||||
|
|
||||||
|
def grab_device(device):
|
||||||
|
"""Grabs the respective device via evtest."""
|
||||||
|
|
||||||
|
return run((EVTEST, '--grab', device))
|
||||||
|
|
||||||
|
|
||||||
|
def get_devices():
|
||||||
|
"""Reads the device from the config file."""
|
||||||
|
|
||||||
|
parser = ConfigParser()
|
||||||
|
parser.read(CONFIG_FILE)
|
||||||
|
|
||||||
|
with suppress(KeyError):
|
||||||
|
yield parser['Devices']['keyboard']
|
||||||
|
|
||||||
|
with suppress(KeyError):
|
||||||
|
yield parser['Devices']['touchpad']
|
||||||
|
|
||||||
|
|
||||||
|
def disable_devices(devices):
|
||||||
|
"""Disables the given devices."""
|
||||||
|
|
||||||
|
threads = []
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
thread = Thread(target=grab_device, args=[device])
|
||||||
|
threads.append(thread)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
for thread in threads:
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
disable_devices(get_devices())
|
|
@ -2,6 +2,6 @@
|
||||||
Comment=Toggle tablet mode
|
Comment=Toggle tablet mode
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Name=Tablet Mode
|
Name=Tablet Mode
|
||||||
Exec=/usr/local/bin/setsysmode toggle
|
Exec=/usr/bin/toggle-tablet-mode
|
||||||
Type=Application
|
Type=Application
|
||||||
Icon=pda-symbolic
|
Icon=pda-symbolic
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"tablet": [
|
|
||||||
"/dev/input/by-path/platform-i8042-serio-0-event-kbd",
|
|
||||||
"/dev/input/by-path/pci-0000:00:14.0-usb-0:9:1.0-event-mouse"
|
|
||||||
],
|
|
||||||
"notify": false
|
|
||||||
}
|
|
|
@ -1,10 +1,6 @@
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Configure system for tablet mode
|
Description=Disable keyboard and touchpad for tablet mode
|
||||||
Conflicts=laptop-mode.service
|
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/local/bin/sysmoded tablet
|
ExecStart=/usr/bin/tablet-mode
|
||||||
StandardOutput=null
|
StandardOutput=null
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
|
@ -1,13 +1 @@
|
||||||
# This file is part of tablet-mode.
|
%tablet ALL=(ALL) NOPASSWD: /usr/bin/systemctl start tablet-mode.service, /usr/bin/systemctl stop tablet-mode.service
|
||||||
#
|
|
||||||
# It allows users in the group "tablet" to toggle
|
|
||||||
# the system between tablet and laptop mode.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
Cmnd_Alias START_LAPTOP_MODE = /usr/bin/systemctl start laptop-mode.service
|
|
||||||
Cmnd_Alias STOP_LAPTOP_MODE = /usr/bin/systemctl stop laptop-mode.service
|
|
||||||
Cmnd_Alias START_TABLET_MODE = /usr/bin/systemctl start tablet-mode.service
|
|
||||||
Cmnd_Alias STOP_TABLET_MODE = /usr/bin/systemctl stop tablet-mode.service
|
|
||||||
|
|
||||||
%tablet ALL=(ALL) NOPASSWD: START_LAPTOP_MODE, STOP_LAPTOP_MODE, START_TABLET_MODE, STOP_TABLET_MODE
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
"""Tablet mode library."""
|
|
|
@ -1,130 +0,0 @@
|
||||||
"""Sets the system mode."""
|
|
||||||
|
|
||||||
from argparse import ArgumentParser, Namespace
|
|
||||||
from subprocess import DEVNULL
|
|
||||||
from subprocess import CalledProcessError
|
|
||||||
from subprocess import CompletedProcess
|
|
||||||
from subprocess import check_call
|
|
||||||
from subprocess import run
|
|
||||||
from sys import stderr
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from tabletmode.config import load_config
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTION = 'Sets or toggles the system mode.'
|
|
||||||
LAPTOP_MODE_SERVICE = 'laptop-mode.service'
|
|
||||||
TABLET_MODE_SERVICE = 'tablet-mode.service'
|
|
||||||
SUDO = '/usr/bin/sudo'
|
|
||||||
|
|
||||||
|
|
||||||
def get_args() -> Namespace:
|
|
||||||
"""Returns the CLI arguments."""
|
|
||||||
|
|
||||||
parser = ArgumentParser(description=DESCRIPTION)
|
|
||||||
parser.add_argument(
|
|
||||||
'-n', '--notify', action='store_true',
|
|
||||||
help='display an on-screen notification')
|
|
||||||
subparsers = parser.add_subparsers(dest='mode')
|
|
||||||
subparsers.add_parser('toggle', help='toggles the system mode')
|
|
||||||
subparsers.add_parser('laptop', help='switch to laptop mode')
|
|
||||||
subparsers.add_parser('tablet', help='switch to tablet mode')
|
|
||||||
subparsers.add_parser('default', help='do not disable any input devices')
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def systemctl(action: str, unit: str, *, root: bool = False,
|
|
||||||
sudo: str = SUDO) -> bool:
|
|
||||||
"""Runs systemctl."""
|
|
||||||
|
|
||||||
command = [sudo] if root else []
|
|
||||||
command += ['systemctl', action, unit]
|
|
||||||
|
|
||||||
try:
|
|
||||||
check_call(command, stdout=DEVNULL) # Return 0 on success.
|
|
||||||
except CalledProcessError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def notify_send(summary: str, body: Optional[str] = None) -> CompletedProcess:
|
|
||||||
"""Sends the respective message."""
|
|
||||||
|
|
||||||
command = ['/usr/bin/notify-send', summary]
|
|
||||||
|
|
||||||
if body is not None:
|
|
||||||
command.append(body)
|
|
||||||
|
|
||||||
return run(command, stdout=DEVNULL, check=False)
|
|
||||||
|
|
||||||
|
|
||||||
def notify_laptop_mode() -> CompletedProcess:
|
|
||||||
"""Notifies about laptop mode."""
|
|
||||||
|
|
||||||
return notify_send('Laptop mode.', 'The system is now in laptop mode.')
|
|
||||||
|
|
||||||
|
|
||||||
def notify_tablet_mode() -> CompletedProcess:
|
|
||||||
"""Notifies about tablet mode."""
|
|
||||||
|
|
||||||
return notify_send('Tablet mode.', 'The system is now in tablet mode.')
|
|
||||||
|
|
||||||
|
|
||||||
def default_mode(notify: bool = False, *, sudo: str = SUDO) -> None:
|
|
||||||
"""Restores all blocked input devices."""
|
|
||||||
|
|
||||||
systemctl('stop', LAPTOP_MODE_SERVICE, root=True, sudo=sudo)
|
|
||||||
systemctl('stop', TABLET_MODE_SERVICE, root=True, sudo=sudo)
|
|
||||||
|
|
||||||
if notify:
|
|
||||||
notify_send('Default mode.', 'The system is now in default mode.')
|
|
||||||
|
|
||||||
|
|
||||||
def laptop_mode(notify: bool = False, *, sudo: str = SUDO) -> None:
|
|
||||||
"""Starts the laptop mode."""
|
|
||||||
|
|
||||||
systemctl('stop', TABLET_MODE_SERVICE, root=True, sudo=sudo)
|
|
||||||
systemctl('start', LAPTOP_MODE_SERVICE, root=True, sudo=sudo)
|
|
||||||
|
|
||||||
if notify:
|
|
||||||
notify_laptop_mode()
|
|
||||||
|
|
||||||
|
|
||||||
def tablet_mode(notify: bool = False, *, sudo: str = SUDO) -> None:
|
|
||||||
"""Starts the tablet mode."""
|
|
||||||
|
|
||||||
systemctl('stop', LAPTOP_MODE_SERVICE, root=True, sudo=sudo)
|
|
||||||
systemctl('start', TABLET_MODE_SERVICE, root=True, sudo=sudo)
|
|
||||||
|
|
||||||
if notify:
|
|
||||||
notify_tablet_mode()
|
|
||||||
|
|
||||||
|
|
||||||
def toggle_mode(notify: bool = False, *, sudo: str = SUDO) -> None:
|
|
||||||
"""Toggles between laptop and tablet mode."""
|
|
||||||
|
|
||||||
if systemctl('status', TABLET_MODE_SERVICE):
|
|
||||||
laptop_mode(notify=notify, sudo=sudo)
|
|
||||||
else:
|
|
||||||
tablet_mode(notify=notify, sudo=sudo)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""Runs the main program."""
|
|
||||||
|
|
||||||
args = get_args()
|
|
||||||
config = load_config()
|
|
||||||
notify = config.get('notify', False) or args.notify
|
|
||||||
sudo = config.get('sudo', SUDO)
|
|
||||||
|
|
||||||
if args.mode == 'toggle':
|
|
||||||
toggle_mode(notify=notify, sudo=sudo)
|
|
||||||
elif args.mode == 'default':
|
|
||||||
default_mode(notify=notify, sudo=sudo)
|
|
||||||
elif args.mode == 'laptop':
|
|
||||||
laptop_mode(notify=notify, sudo=sudo)
|
|
||||||
elif args.mode == 'tablet':
|
|
||||||
tablet_mode(notify=notify, sudo=sudo)
|
|
||||||
else:
|
|
||||||
print('Must specify a mode.', file=stderr, flush=True)
|
|
|
@ -1,23 +0,0 @@
|
||||||
"""Configuration file parsing."""
|
|
||||||
|
|
||||||
from json import load
|
|
||||||
from logging import getLogger
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['load_config']
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_FILE = Path('/etc/tablet-mode.json')
|
|
||||||
LOGGER = getLogger('tabletmode')
|
|
||||||
|
|
||||||
|
|
||||||
def load_config() -> dict:
|
|
||||||
"""Returns the configuration."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with CONFIG_FILE.open('r') as cfg:
|
|
||||||
return load(cfg)
|
|
||||||
except FileNotFoundError:
|
|
||||||
LOGGER.warning('Config file %s does not exist.', CONFIG_FILE)
|
|
||||||
return {}
|
|
|
@ -1,68 +0,0 @@
|
||||||
"""System mode daemon."""
|
|
||||||
|
|
||||||
from argparse import ArgumentParser, Namespace
|
|
||||||
from logging import DEBUG, INFO, basicConfig, getLogger
|
|
||||||
from subprocess import Popen
|
|
||||||
from typing import Iterable
|
|
||||||
|
|
||||||
from tabletmode.config import load_config
|
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTION = 'Setup system for laptop or tablet mode.'
|
|
||||||
EVTEST = '/usr/bin/evtest'
|
|
||||||
LOG_FORMAT = '[%(levelname)s] %(name)s: %(message)s'
|
|
||||||
LOGGER = getLogger('sysmoded')
|
|
||||||
|
|
||||||
|
|
||||||
def get_args() -> Namespace:
|
|
||||||
"""Parses the CLI arguments."""
|
|
||||||
|
|
||||||
parser = ArgumentParser(description=DESCRIPTION)
|
|
||||||
parser.add_argument(
|
|
||||||
'-v', '--verbose', action='store_true',
|
|
||||||
help='turn on verbose logging')
|
|
||||||
subparsers = parser.add_subparsers(dest='mode')
|
|
||||||
subparsers.add_parser('laptop', help='enable laptop mode')
|
|
||||||
subparsers.add_parser('tablet', help='enable tablet mode')
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def disable_device(device: str) -> Popen:
|
|
||||||
"""Disables the respective device via evtest."""
|
|
||||||
|
|
||||||
return Popen((EVTEST, '--grab', device))
|
|
||||||
|
|
||||||
|
|
||||||
def disable_devices(devices: Iterable[str]) -> None:
|
|
||||||
"""Disables the given devices."""
|
|
||||||
|
|
||||||
subprocesses = []
|
|
||||||
|
|
||||||
for device in devices:
|
|
||||||
subprocess = disable_device(device)
|
|
||||||
subprocesses.append(subprocess)
|
|
||||||
|
|
||||||
for subprocess in subprocesses:
|
|
||||||
subprocess.wait()
|
|
||||||
|
|
||||||
|
|
||||||
def get_devices(mode: str) -> Iterable[str]:
|
|
||||||
"""Reads the device from the config file."""
|
|
||||||
|
|
||||||
config = load_config()
|
|
||||||
devices = config.get(mode) or ()
|
|
||||||
|
|
||||||
if not devices:
|
|
||||||
LOGGER.info('No devices configured to disable.')
|
|
||||||
|
|
||||||
return devices
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Runs the main program."""
|
|
||||||
|
|
||||||
arguments = get_args()
|
|
||||||
level = DEBUG if arguments.verbose else INFO
|
|
||||||
basicConfig(level=level, format=LOG_FORMAT)
|
|
||||||
devices = get_devices(arguments.mode)
|
|
||||||
disable_devices(devices)
|
|
10
toggle-tablet-mode
Executable file
10
toggle-tablet-mode
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
if ( systemctl status tablet-mode.service ); then
|
||||||
|
sudo systemctl stop tablet-mode.service
|
||||||
|
notify-send "Laptop mode" "The system is now in laptop mode."
|
||||||
|
else
|
||||||
|
sudo systemctl start tablet-mode.service
|
||||||
|
notify-send "Tablet mode" "The system is now in tablet mode."
|
||||||
|
fi
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue