Compare commits

...

21 commits
1.0.1 ... main

Author SHA1 Message Date
5488f154e1 Only add group and user if they don't exist 2023-07-16 13:03:10 +02:00
5a8657d7ca Add link to upstream repo 2023-07-16 12:31:00 +02:00
45b5790e79 Add hint about reboot 2023-07-16 12:29:02 +02:00
902e5bebe4 Hint about OS 2023-07-16 12:23:18 +02:00
8b0a4c954c Update README 2023-07-16 12:21:58 +02:00
c1914511ee Add install script
Add Minibook configuration
Re-Add scripts
2023-07-16 12:17:59 +02:00
Richard Neumann
d24cc31b04 Allow drop-in for sudo, such as doas 2021-05-14 21:08:01 +02:00
Richard Neumann
6f3f4a2764 Migrated to setuptools-scm. 2021-01-12 15:21:40 +01:00
Richard Neumann
cdcd82fdfd Removed newline at top of files. 2021-01-05 11:41:43 +01:00
Richard Neumann
14fd640059 Migrated scripts to entry points. 2021-01-05 11:37:19 +01:00
Richard Neumann
501f28e0ee Added type hints. 2021-01-05 11:36:46 +01:00
Richard Neumann
d46be0bc0b Fixed mode notification and refactored mode toggle. 2020-04-28 21:54:37 +02:00
Richard Neumann
ffd6978317 Updated README. 2020-03-26 11:49:57 +01:00
Richard Neumann
309bafd5d1 Refactored python code into library. 2020-03-26 11:47:41 +01:00
Richard Neumann
aae65e3825 Updated README. 2019-09-03 23:45:24 +02:00
Richard Neumann
99e8c6da90 Updated readme. 2019-09-02 11:58:39 +02:00
Richard Neumann
72733caa7b Fix. 2019-06-08 08:51:21 +02:00
Richard Neumann
df32a93912 Fix. 2019-06-01 23:49:40 +02:00
Richard Neumann
4a2ed05182 Fix. 2019-05-21 15:32:39 +02:00
Richard Neumann
359fecb027 Refac. 2019-05-21 15:26:30 +02:00
Richard Neumann
8aef536527
Update 2019-03-23 15:26:21 +01:00
16 changed files with 342 additions and 72 deletions

View file

@ -1,16 +1,13 @@
# tablet-mode
# Chuwi Minibook tablet mode switch
Allow users to toggle a convertible laptop between laptop and tablet mode.
Allow users to toggle a Chuwi Minibook laptop between laptop and tablet mode.
This is based on https://github.com/conqp/tablet-mode.
## Configuration
## Installation
The keyboard device to be deactivated in tablet mode must be specified in `/etc/tablet-mode.conf`:
[Devices]
keyboard = /dev/input/by-path/platform-i8042-serio-0-event-kbd
touchpad = /dev/input/by-path/platform-i8042-serio-1-event-mouse
Just run `install.sh` on your Debian system and enter the name of you local user account.
Reboot after successful installation.
## Usage
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.
You can toggle between tablet and laptop mode by running `setsysmode toggle` or use the desktop icon provided with this package.

35
install.sh Executable file
View file

@ -0,0 +1,35 @@
#!/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

10
laptop-mode.service Normal file
View file

@ -0,0 +1,10 @@
[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

8
setsysmode Normal file
View file

@ -0,0 +1,8 @@
#! /usr/bin/env python3
"""Sets the system mode."""
from tabletmode.cli import main
if __name__ == '__main__':
main()

25
setup.py Executable file
View file

@ -0,0 +1,25 @@
#! /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 Normal file
View file

@ -0,0 +1,8 @@
#! /usr/bin/env python3
"""System mode daemon."""
from tabletmode.daemon import main
if __name__ == '__main__':
main()

View file

@ -1,48 +0,0 @@
#! /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())

View file

@ -2,6 +2,6 @@
Comment=Toggle tablet mode
Terminal=false
Name=Tablet Mode
Exec=/usr/bin/toggle-tablet-mode
Exec=/usr/local/bin/setsysmode toggle
Type=Application
Icon=pda-symbolic

7
tablet-mode.json Normal file
View file

@ -0,0 +1,7 @@
{
"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
}

View file

@ -1,6 +1,10 @@
[Unit]
Description=Disable keyboard and touchpad for tablet mode
Description=Configure system for tablet mode
Conflicts=laptop-mode.service
[Service]
ExecStart=/usr/bin/tablet-mode
ExecStart=/usr/local/bin/sysmoded tablet
StandardOutput=null
[Install]
WantedBy=multi-user.target

View file

@ -1 +1,13 @@
%tablet ALL=(ALL) NOPASSWD: /usr/bin/systemctl start tablet-mode.service, /usr/bin/systemctl stop tablet-mode.service
# This file is part of tablet-mode.
#
# 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
tabletmode/__init__.py Normal file
View file

@ -0,0 +1 @@
"""Tablet mode library."""

130
tabletmode/cli.py Normal file
View file

@ -0,0 +1,130 @@
"""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)

23
tabletmode/config.py Normal file
View file

@ -0,0 +1,23 @@
"""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 {}

68
tabletmode/daemon.py Normal file
View file

@ -0,0 +1,68 @@
"""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)

View file

@ -1,10 +0,0 @@
#! /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