From 223b9f2721de65772d51443c5d7865ceba2b7ead Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 25 Feb 2023 11:15:09 +0330 Subject: [PATCH 01/45] Add common utility Because: - These are commonly used funcionalities inside bash scripts Usage: - This file sould be sourced in the script file Refrence: https://github.com/ralish/bash-script-template --- src/scripts/utils/source.sh | 394 ++++++++++++++++++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 src/scripts/utils/source.sh diff --git a/src/scripts/utils/source.sh b/src/scripts/utils/source.sh new file mode 100644 index 0000000..8a63a7d --- /dev/null +++ b/src/scripts/utils/source.sh @@ -0,0 +1,394 @@ +#!/usr/bin/env bash + +# DESC: Handler for unexpected errors +# ARGS: $1 (optional): Exit code (defaults to 1) +# OUTS: None +function script_trap_err() { + local exit_code=1 + + # Disable the error trap handler to prevent potential recursion + trap - ERR + + # Consider any further errors non-fatal to ensure we run to completion + set +o errexit + set +o pipefail + + # Validate any provided exit code + if [[ ${1-} =~ ^[0-9]+$ ]]; then + exit_code="$1" + fi + + # Output debug data if in Cron mode + if [[ -n ${cron-} ]]; then + # Restore original file output descriptors + if [[ -n ${script_output-} ]]; then + exec 1>&3 2>&4 + fi + + # Print basic debugging information + printf '%b\n' "$ta_none" + printf '***** Abnormal termination of script *****\n' + printf 'Script Path: %s\n' "$script_path" + printf 'Script Parameters: %s\n' "$script_params" + printf 'Script Exit Code: %s\n' "$exit_code" + + # Print the script log if we have it. It's possible we may not if we + # failed before we even called cron_init(). This can happen if bad + # parameters were passed to the script so we bailed out very early. + if [[ -n ${script_output-} ]]; then + # shellcheck disable=SC2312 + printf 'Script Output:\n\n%s' "$(cat "$script_output")" + else + printf 'Script Output: None (failed before log init)\n' + fi + fi + + # Exit with failure status + exit "$exit_code" +} + +# DESC: Handler for exiting the script +# ARGS: None +# OUTS: None +function script_trap_exit() { + cd "$orig_cwd" + + # Remove Cron mode script log + if [[ -n ${cron-} && -f ${script_output-} ]]; then + rm "$script_output" + fi + + # Remove script execution lock + if [[ -d ${script_lock-} ]]; then + rmdir "$script_lock" + fi + + # Restore terminal colours + printf '%b' "$ta_none" +} + +# DESC: Exit script with the given message +# ARGS: $1 (required): Message to print on exit +# $2 (optional): Exit code (defaults to 0) +# OUTS: None +# NOTE: The convention used in this script for exit codes is: +# 0: Normal exit +# 1: Abnormal exit due to external error +# 2: Abnormal exit due to script error +function script_exit() { + if [[ $# -eq 1 ]]; then + printf '%s\n' "$1" + exit 0 + fi + + if [[ ${2-} =~ ^[0-9]+$ ]]; then + printf '%b\n' "$1" + # If we've been provided a non-zero exit code run the error trap + if [[ $2 -ne 0 ]]; then + script_trap_err "$2" + else + exit 0 + fi + fi + + script_exit 'Missing required argument to script_exit()!' 2 +} + +# DESC: Generic script initialisation +# ARGS: $@ (optional): Arguments provided to the script +# OUTS: $orig_cwd: The current working directory when the script was run +# $script_path: The full path to the script +# $script_dir: The directory path of the script +# $script_name: The file name of the script +# $script_params: The original parameters provided to the script +# $ta_none: The ANSI control code to reset all text attributes +# NOTE: $script_path only contains the path that was used to call the script +# and will not resolve any symlinks which may be present in the path. +# You can use a tool like realpath to obtain the "true" path. The same +# caveat applies to both the $script_dir and $script_name variables. +# shellcheck disable=SC2034 +function script_init() { + # Useful variables + readonly orig_cwd="$PWD" + readonly script_params="$*" + readonly script_path="${BASH_SOURCE[1]}" + script_dir="$(dirname "$script_path")" + script_name="$(basename "$script_path")" + readonly script_dir script_name + + # Important to always set as we use it in the exit handler + # shellcheck disable=SC2155 + readonly ta_none="$(tput sgr0 2> /dev/null || true)" +} + +# DESC: Initialise colour variables +# ARGS: None +# OUTS: Read-only variables with ANSI control codes +# NOTE: If --no-colour was set the variables will be empty. The output of the +# $ta_none variable after each tput is redundant during normal execution, +# but ensures the terminal output isn't mangled when running with xtrace. +# shellcheck disable=SC2034,SC2155 +function colour_init() { + if [[ -z ${no_colour-} ]]; then + # Text attributes + readonly ta_bold="$(tput bold 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly ta_uscore="$(tput smul 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly ta_blink="$(tput blink 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly ta_reverse="$(tput rev 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly ta_conceal="$(tput invis 2> /dev/null || true)" + printf '%b' "$ta_none" + + # Foreground codes + readonly fg_black="$(tput setaf 0 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly fg_blue="$(tput setaf 4 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly fg_cyan="$(tput setaf 6 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly fg_green="$(tput setaf 2 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly fg_magenta="$(tput setaf 5 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly fg_red="$(tput setaf 1 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly fg_white="$(tput setaf 7 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly fg_yellow="$(tput setaf 3 2> /dev/null || true)" + printf '%b' "$ta_none" + + # Background codes + readonly bg_black="$(tput setab 0 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly bg_blue="$(tput setab 4 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly bg_cyan="$(tput setab 6 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly bg_green="$(tput setab 2 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly bg_magenta="$(tput setab 5 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly bg_red="$(tput setab 1 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly bg_white="$(tput setab 7 2> /dev/null || true)" + printf '%b' "$ta_none" + readonly bg_yellow="$(tput setab 3 2> /dev/null || true)" + printf '%b' "$ta_none" + else + # Text attributes + readonly ta_bold='' + readonly ta_uscore='' + readonly ta_blink='' + readonly ta_reverse='' + readonly ta_conceal='' + + # Foreground codes + readonly fg_black='' + readonly fg_blue='' + readonly fg_cyan='' + readonly fg_green='' + readonly fg_magenta='' + readonly fg_red='' + readonly fg_white='' + readonly fg_yellow='' + + # Background codes + readonly bg_black='' + readonly bg_blue='' + readonly bg_cyan='' + readonly bg_green='' + readonly bg_magenta='' + readonly bg_red='' + readonly bg_white='' + readonly bg_yellow='' + fi +} + +# DESC: Initialise Cron mode +# ARGS: None +# OUTS: $script_output: Path to the file stdout & stderr was redirected to +function cron_init() { + if [[ -n ${cron-} ]]; then + # Redirect all output to a temporary file + script_output="$(mktemp --tmpdir "$script_name".XXXXX)" + readonly script_output + exec 3>&1 4>&2 1> "$script_output" 2>&1 + fi +} + +# DESC: Acquire script lock +# ARGS: $1 (optional): Scope of script execution lock (system or user) +# OUTS: $script_lock: Path to the directory indicating we have the script lock +# NOTE: This lock implementation is extremely simple but should be reliable +# across all platforms. It does *not* support locking a script with +# symlinks or multiple hardlinks as there's no portable way of doing so. +# If the lock was acquired it's automatically released on script exit. +function lock_init() { + local lock_dir + if [[ $1 = 'system' ]]; then + lock_dir="/tmp/$script_name.lock" + elif [[ $1 = 'user' ]]; then + lock_dir="/tmp/$script_name.$UID.lock" + else + script_exit 'Missing or invalid argument to lock_init()!' 2 + fi + + if mkdir "$lock_dir" 2> /dev/null; then + readonly script_lock="$lock_dir" + verbose_print "Acquired script lock: $script_lock" + else + script_exit "Unable to acquire script lock: $lock_dir" 1 + fi +} + +# DESC: Pretty print the provided string +# ARGS: $1 (required): Message to print (defaults to a green foreground) +# $2 (optional): Colour to print the message with. This can be an ANSI +# escape code or one of the prepopulated colour variables. +# $3 (optional): Set to any value to not append a new line to the message +# OUTS: None +function pretty_print() { + if [[ $# -lt 1 ]]; then + script_exit 'Missing required argument to pretty_print()!' 2 + fi + + if [[ -z ${no_colour-} ]]; then + if [[ -n ${2-} ]]; then + printf '%b' "$2" + else + printf '%b' "$fg_green" + fi + fi + + # Print message & reset text attributes + if [[ -n ${3-} ]]; then + printf '%s%b' "$1" "$ta_none" + else + printf '%s%b\n' "$1" "$ta_none" + fi +} + +# DESC: Only pretty_print() the provided string if verbose mode is enabled +# ARGS: $@ (required): Passed through to pretty_print() function +# OUTS: None +function verbose_print() { + if [[ -n ${verbose-} ]]; then + pretty_print "$@" + fi +} + +# DESC: Combines two path variables and removes any duplicates +# ARGS: $1 (required): Path(s) to join with the second argument +# $2 (optional): Path(s) to join with the first argument +# OUTS: $build_path: The constructed path +# NOTE: Heavily inspired by: https://unix.stackexchange.com/a/40973 +function build_path() { + if [[ $# -lt 1 ]]; then + script_exit 'Missing required argument to build_path()!' 2 + fi + + local new_path path_entry temp_path + + temp_path="$1:" + if [[ -n ${2-} ]]; then + temp_path="$temp_path$2:" + fi + + new_path= + while [[ -n $temp_path ]]; do + path_entry="${temp_path%%:*}" + case "$new_path:" in + *:"$path_entry":*) ;; + *) + new_path="$new_path:$path_entry" + ;; + esac + temp_path="${temp_path#*:}" + done + + # shellcheck disable=SC2034 + build_path="${new_path#:}" +} + +# DESC: Check a binary exists in the search path +# ARGS: $1 (required): Name of the binary to test for existence +# $2 (optional): Set to any value to treat failure as a fatal error +# OUTS: None +function check_binary() { + if [[ $# -lt 1 ]]; then + script_exit 'Missing required argument to check_binary()!' 2 + fi + + if ! command -v "$1" > /dev/null 2>&1; then + if [[ -n ${2-} ]]; then + script_exit "Missing dependency: Couldn't locate $1." 1 + else + verbose_print "Missing dependency: $1" "${fg_red-}" + return 1 + fi + fi + + verbose_print "Found dependency: $1" + return 0 +} + +# DESC: Validate we have superuser access as root (via sudo if requested) +# ARGS: $1 (optional): Set to any value to not attempt root access via sudo +# OUTS: None +function check_superuser() { + local superuser + if [[ $EUID -eq 0 ]]; then + superuser=true + elif [[ -z ${1-} ]]; then + # shellcheck disable=SC2310 + if check_binary sudo; then + verbose_print 'Sudo: Updating cached credentials ...' + if ! sudo -v; then + verbose_print "Sudo: Couldn't acquire credentials ..." \ + "${fg_red-}" + else + local test_euid + test_euid="$(sudo -H -- "$BASH" -c 'printf "%s" "$EUID"')" + if [[ $test_euid -eq 0 ]]; then + superuser=true + fi + fi + fi + fi + + if [[ -z ${superuser-} ]]; then + verbose_print 'Unable to acquire superuser credentials.' "${fg_red-}" + return 1 + fi + + verbose_print 'Successfully acquired superuser credentials.' + return 0 +} + +# DESC: Run the requested command as root (via sudo if requested) +# ARGS: $1 (optional): Set to zero to not attempt execution via sudo +# $@ (required): Passed through for execution as root user +# OUTS: None +function run_as_root() { + if [[ $# -eq 0 ]]; then + script_exit 'Missing required argument to run_as_root()!' 2 + fi + + if [[ ${1-} =~ ^0$ ]]; then + local skip_sudo=true + shift + fi + + if [[ $EUID -eq 0 ]]; then + "$@" + elif [[ -z ${skip_sudo-} ]]; then + sudo -H -- "$@" + else + script_exit "Unable to run requested command as root: $*" 1 + fi +} + +# vim: syntax=sh cc=80 tw=79 ts=4 sw=4 sts=4 et sr \ No newline at end of file From ffab23f13e240297d392b78b80c0b0e7a4037ee5 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 25 Feb 2023 15:40:48 +0330 Subject: [PATCH 02/45] Add abstract monitor handler Because: - provide common functionality of monitor observer. USB and Monitor handlers will be implemented later. --- src/scripts/python/Handler.py | 46 +++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/scripts/python/Handler.py diff --git a/src/scripts/python/Handler.py b/src/scripts/python/Handler.py new file mode 100644 index 0000000..3aed6c8 --- /dev/null +++ b/src/scripts/python/Handler.py @@ -0,0 +1,46 @@ +from abc import ABC, abstractmethod +from pyudev import Context, Monitor, MonitorObserver + + +class Handler(ABC): + """Abstract Handler calss for device monitoring + + NOTE: No checking are done for overlaping filters and callback will be + even by multiple handlers. + + Args: + ABC: Abstract Base Class, provides abstract method functionality and + readability. + """ + + def __init__(self, filter) -> None: + """Initiate a monitor observer and applies `filter` if any provided + + Args: + filter (_type_): _description_ + """ + monitor = Monitor.from_netlink(Context()) + if filter: + monitor.filter_by(filter) + self.observer = MonitorObserver(monitor, callback=self.handler) + self.observer.start() + + @abstractmethod + def callback(self, device): + """Callback + + This method must be implemented by child calsses. This method is + responsible for further managments of the devices related to its filter. + + Args: + device (pyudev.Device): device passed by observer through handler + """ + raise NotImplemented("Callback MUST be implemented") + + def handler(self, device): + """wrapper around callback implemented + + Args: + device (pyudev.Device): modified device passed by self.observer + """ + self.callback(device) \ No newline at end of file From 91d43a68149ad023bfb12c72bfee25cfb0acec65 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 25 Feb 2023 15:43:47 +0330 Subject: [PATCH 03/45] Rename module to udevhandle module is renamed for more readablility. Also it would contain more bussiness logic and not the Hanlder class alone. --- src/scripts/python/{Handler.py => udevhandle.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/scripts/python/{Handler.py => udevhandle.py} (100%) diff --git a/src/scripts/python/Handler.py b/src/scripts/python/udevhandle.py similarity index 100% rename from src/scripts/python/Handler.py rename to src/scripts/python/udevhandle.py From 803e8f4de5562238d2de01863a7dbf316262e104 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 25 Feb 2023 16:06:47 +0330 Subject: [PATCH 04/45] Add UsbHandler This handler runs callback on usb events. When a usb is connected following events will capture: - add - change - add - bind - bind and when usb mouse is removed following will captured: - unbind - remove - unbind - remove these effects are tested with device logitech (R) B100 mouse. --- src/scripts/python/udevhandle.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/scripts/python/udevhandle.py b/src/scripts/python/udevhandle.py index 3aed6c8..5891dac 100644 --- a/src/scripts/python/udevhandle.py +++ b/src/scripts/python/udevhandle.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from pyudev import Context, Monitor, MonitorObserver +from pyudev import Context, Monitor, MonitorObserver, Device class Handler(ABC): @@ -26,7 +26,7 @@ class Handler(ABC): self.observer.start() @abstractmethod - def callback(self, device): + def callback(self, device: Device): """Callback This method must be implemented by child calsses. This method is @@ -43,4 +43,23 @@ class Handler(ABC): Args: device (pyudev.Device): modified device passed by self.observer """ - self.callback(device) \ No newline at end of file + self.callback(device) + + +class MouseHandler(Handler): + def __init__(self) -> None: + """Initiate UsbHanlder + + Initialization contains two major steps. First it would do a + configuration for currently available devices and then it would wait for + USB udev events to reconfigure the settings. configurations would be + done by (This part is not decided yet. it could be done by BASH SCRIPTS + or we can invoke xinput binaries via python itself. a bash script + solution would be benefitial since it can used as utility). + """ + # TODO: use somthing that only captures + super().__init__('usb') + + + def callback(self, device): + print(device.action) \ No newline at end of file From 6fd2ad454e0e37b5dc694ca195990c0567e228a6 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 25 Feb 2023 16:07:17 +0330 Subject: [PATCH 05/45] Add python redundant files to gitignore --- .gitignore | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/.gitignore b/.gitignore index 4ddbd9c..8fe95e7 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,158 @@ _deps # .nfs files are created when an open file is removed but is still being accessed .nfs* + +# ------> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ From 29867682ef71c00c630f63a6593e85929d87da4a Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 27 Feb 2023 09:55:51 +0330 Subject: [PATCH 06/45] Add xinput short list In this commit module `xutil` is added which will contain utility functions relatd to xorg (e.g. xinput, xrandr and ...). modules and classes inside xutil are wrappers around those binaries. --- src/scripts/python/xutil/common.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/scripts/python/xutil/common.py diff --git a/src/scripts/python/xutil/common.py b/src/scripts/python/xutil/common.py new file mode 100644 index 0000000..71aca76 --- /dev/null +++ b/src/scripts/python/xutil/common.py @@ -0,0 +1,10 @@ +import subprocess + +ENCODING = 'utf-8' + +def get_list_short(): + """Returns string output of the `xinput --list --short` command encoded as + UTF-8""" + completed = subprocess.run( + ['xinput', '--list', '--short'], capture_output=True) + return completed.stdout.decode(ENCODING) From c42fd13145899ed74a09907c536d074b7d822940 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 27 Feb 2023 10:28:17 +0330 Subject: [PATCH 07/45] Add id-wise list short Fn. get_list_short(id) is added because it outputs simpler syntax and per-case output. --- src/scripts/python/xutil/common.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/scripts/python/xutil/common.py b/src/scripts/python/xutil/common.py index 71aca76..0906c88 100644 --- a/src/scripts/python/xutil/common.py +++ b/src/scripts/python/xutil/common.py @@ -1,10 +1,39 @@ import subprocess +from typing import List +from multipledispatch import dispatch ENCODING = 'utf-8' +@dispatch() def get_list_short(): """Returns string output of the `xinput --list --short` command encoded as UTF-8""" completed = subprocess.run( ['xinput', '--list', '--short'], capture_output=True) return completed.stdout.decode(ENCODING) + + +@dispatch(int) +def get_list_short(id): + """Short List of the id + + Args: + id (int): id registered in xinput + + Rises: + ValueError: in case of id not found in devices + """ + completed = subprocess.run( + ['xinput', '--list', '--short', str(id)], capture_output=True) + + if completed.returncode == 0: + return completed.stdout.decode(ENCODING) + else: + ValueError(f'id[{id}] is not registered') + + +def get_ids() -> List[int]: + """returns list of ids registered in xinput""" + completed = subprocess.run( + ['xinput', '--list', '--id-only'], capture_output=True) + return list(map(int), completed.decode(ENCODING)) \ No newline at end of file From ee141082c227f353b1269fa3a477cef3c46ddee9 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 27 Feb 2023 10:58:01 +0330 Subject: [PATCH 08/45] Fix map bug wrong paranthesis for map function --- src/scripts/python/xutil/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scripts/python/xutil/common.py b/src/scripts/python/xutil/common.py index 0906c88..fc02c23 100644 --- a/src/scripts/python/xutil/common.py +++ b/src/scripts/python/xutil/common.py @@ -36,4 +36,4 @@ def get_ids() -> List[int]: """returns list of ids registered in xinput""" completed = subprocess.run( ['xinput', '--list', '--id-only'], capture_output=True) - return list(map(int), completed.decode(ENCODING)) \ No newline at end of file + return list(map(int, completed.stdout.decode(ENCODING).split())) \ No newline at end of file From bc145908586ecdadcd00172eef896b792ca7d8f4 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 27 Feb 2023 11:21:11 +0330 Subject: [PATCH 09/45] Add Pointer class This class is a wrapper around xinput results. At this point it has common attributes (name, id, master/slave). --- src/scripts/python/xutil/pointer.py | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/scripts/python/xutil/pointer.py diff --git a/src/scripts/python/xutil/pointer.py b/src/scripts/python/xutil/pointer.py new file mode 100644 index 0000000..cea1f90 --- /dev/null +++ b/src/scripts/python/xutil/pointer.py @@ -0,0 +1,38 @@ +import xutil.common as com +import re + +class XInput: + """Base XInput class + + Attributes: + name (str): name of the input + id (int): id of the input + is_master (bool): True if device is master + """ + + def __init__(self, name, id, is_master) -> None: + """Initializes the class with name, id and master status + + Args: + name (str): name of the input. No processing is done on the name + id (int): id of the input + is_master (bool): master status of the input device + """ + self.name = name + self.id = id + self.is_master = is_master + + +class Pointer(XInput): + """Pointer class + + This class is a wrapper around xinput commandline --list output. + + + Attrs: + is_master (bool): True if the pointer is a master pointer else False + """ + + def __init__(self, name, id, is_master: bool) -> None: + super().__init__(name, id, is_master) + From da0a4c180faf38cf768235c8875f3ef8b3129cd3 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 27 Feb 2023 11:26:53 +0330 Subject: [PATCH 10/45] Add get_pointers Because: + we need this functionality to list pointers and operate on them later + this list can be filtered later with less string computations --- src/scripts/python/xutil/pointer.py | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/scripts/python/xutil/pointer.py b/src/scripts/python/xutil/pointer.py index cea1f90..9ae405b 100644 --- a/src/scripts/python/xutil/pointer.py +++ b/src/scripts/python/xutil/pointer.py @@ -36,3 +36,56 @@ class Pointer(XInput): def __init__(self, name, id, is_master: bool) -> None: super().__init__(name, id, is_master) +def get_short_pointer(id) -> Pointer: + """Generates Pointer object corresponding to id (short attrs) + + Args: + id (int): pointer id + + Returns: + Pointer: pointer object with name, id and is_master props + + Rises: + ValueError: if id is not reistered with xinput + ValueError: if id is not a pointer id + """ + desc = com.get_list_short(id) + name, props = desc.rsplit('id=', 1) + if "pointer" in props: + is_master = "master" in props + return Pointer( + name.strip(), + props.split(maxsplit=1), + is_master) + else: + raise TypeError(f'id[{id}] is not a pointer id') + + +def get_pointers(is_short=True): + """Wraps pointers in `xinput --list` in Pointer class + + Creation of the pointer is done by getting the list description of + each id. if the is_short arg is True, then short list description will be + used which will provide the class `name`, `is_master` and `id` values. + + Getting this pointers is done by first calling `xinput --list --id-only` to + get ids and then execute `xinput --list {id}` to get the description with + less-complicated output compare to iterating over `xinput --list --short` + line by line (--short option has some special characters that cause overhead + to the system for processing them individually and per-case). + """ + pointers = [] + ids = com.get_ids() + for id in ids: + if is_short: + try: + pointers.append(get_short_pointer(id)) + except TypeError as e: + # ignore if the id is not pointer + pass + except e: + # TODO: logging + pass + else: + pass + return pointers From a62a6168986f49416e1e6c40dba962b7f44040e5 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 27 Feb 2023 11:35:04 +0330 Subject: [PATCH 11/45] Refactor with autopep8 --- src/scripts/python/xutil/common.py | 5 +++-- src/scripts/python/xutil/pointer.py | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/scripts/python/xutil/common.py b/src/scripts/python/xutil/common.py index fc02c23..ec8d9c5 100644 --- a/src/scripts/python/xutil/common.py +++ b/src/scripts/python/xutil/common.py @@ -4,6 +4,7 @@ from multipledispatch import dispatch ENCODING = 'utf-8' + @dispatch() def get_list_short(): """Returns string output of the `xinput --list --short` command encoded as @@ -25,7 +26,7 @@ def get_list_short(id): """ completed = subprocess.run( ['xinput', '--list', '--short', str(id)], capture_output=True) - + if completed.returncode == 0: return completed.stdout.decode(ENCODING) else: @@ -36,4 +37,4 @@ def get_ids() -> List[int]: """returns list of ids registered in xinput""" completed = subprocess.run( ['xinput', '--list', '--id-only'], capture_output=True) - return list(map(int, completed.stdout.decode(ENCODING).split())) \ No newline at end of file + return list(map(int, completed.stdout.decode(ENCODING).split())) diff --git a/src/scripts/python/xutil/pointer.py b/src/scripts/python/xutil/pointer.py index 9ae405b..8330b7a 100644 --- a/src/scripts/python/xutil/pointer.py +++ b/src/scripts/python/xutil/pointer.py @@ -1,6 +1,7 @@ import xutil.common as com import re + class XInput: """Base XInput class @@ -25,7 +26,7 @@ class XInput: class Pointer(XInput): """Pointer class - + This class is a wrapper around xinput commandline --list output. @@ -36,6 +37,7 @@ class Pointer(XInput): def __init__(self, name, id, is_master: bool) -> None: super().__init__(name, id, is_master) + def get_short_pointer(id) -> Pointer: """Generates Pointer object corresponding to id (short attrs) @@ -54,7 +56,7 @@ def get_short_pointer(id) -> Pointer: if "pointer" in props: is_master = "master" in props return Pointer( - name.strip(), + name.strip(), props.split(maxsplit=1), is_master) else: @@ -63,7 +65,7 @@ def get_short_pointer(id) -> Pointer: def get_pointers(is_short=True): """Wraps pointers in `xinput --list` in Pointer class - + Creation of the pointer is done by getting the list description of each id. if the is_short arg is True, then short list description will be used which will provide the class `name`, `is_master` and `id` values. From fd23971bccd09749a80a66292972a9bbff12c963 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 27 Feb 2023 12:00:30 +0330 Subject: [PATCH 12/45] Add reattach add a wrapper function arround `xinput --reattach id master` --- src/scripts/python/xutil/common.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/scripts/python/xutil/common.py b/src/scripts/python/xutil/common.py index ec8d9c5..c7c6a86 100644 --- a/src/scripts/python/xutil/common.py +++ b/src/scripts/python/xutil/common.py @@ -33,6 +33,22 @@ def get_list_short(id): ValueError(f'id[{id}] is not registered') +def reattach(id, master): + """Reattach a device to a master + + Args: + id (str|int): name of the slave or id + master (_type_): _description_ + + TODO: Error handling should be done. BUT, if the master is not a master or + id is not valid, xinput will not do anything and nothing bad will happen :) + """ + completed = subprocess.run( + ['xinput', '--reattach', str(id), str(master)], capture_output=True) + + return completed.returncode + + def get_ids() -> List[int]: """returns list of ids registered in xinput""" completed = subprocess.run( From 1774b48e969d9aba7124384b01c3ff1679706341 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 27 Feb 2023 12:19:04 +0330 Subject: [PATCH 13/45] Add change mouse script This script will be attachd to rules corresponding to change action of mouse/pointer devices. At this time it will gather all pointers add reattach them to the Virtual core pointer (Overhead of this approach can be ignored as we will have very limited number of devices and addition or removal of such devices wouldn't be a common thing. --- src/scripts/python/changemouse.py | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100755 src/scripts/python/changemouse.py diff --git a/src/scripts/python/changemouse.py b/src/scripts/python/changemouse.py new file mode 100755 index 0000000..2f18a7c --- /dev/null +++ b/src/scripts/python/changemouse.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + +"""Change mouse script + +Whenever a mouse is changed to the system, this script will be executed. These +are the rules: + + All regular mouses should connect to the "Virtual core pointer" of the + system + + Touchpanel should be connected to the master "touch-pointer" + +NOTE: master "touch-pointer" should be available on the event of adding the + mouse. This could be reached by adding this master to the system on boot. + + Steps: + + List all pointer as we don't know what pointer is added to the system + + group them by their usage by name rules (As if the pointer is eGalax + touch pointer it should be attached to the touch-pointer o.w. it + should be attached to Vitual core pointer (OR trackball TODO)) + + utility functions to find and group pointers aer available in xutil module. + +NOTE: In case of psyco adds and removes mouses with intervals smaller than run + time of this code (which is not probabale at all) a lockfile is used so that + only one instance of this code is running at a moment so no conflicts occur. +""" + +import xutil.pointer as xup +import xutil.common as com + +if __name__ == "__main__": + # If we didn't import the script as utility + pointers = xup.get_pointers() + # TODO filter functionality inside utilities + core_master = list(filter(lambda x: "Virtual core" in x.name, pointers))[0] + for pointer in filter(lambda x: not x.slave, pointers): + if not "eGalax" in pointer.name: + com.reattach(pointer.id, core_master.id) + else: + # TODO: add touchpannel support + pass From dc9db72e5742de29d010e326952b98a551a6851e Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 27 Feb 2023 13:08:10 +0330 Subject: [PATCH 14/45] Fix mouse change bug BUG: script does not take effect when script is run --- Test Scenario: Two mouse are attached to the different masters and then we execute the script Expected Behavior: both mouses reattach to the Virtual core Problem: filter in the for-loop gathered all masters not slaves --- src/scripts/python/changemouse.py | 8 ++++---- src/scripts/python/xutil/pointer.py | 6 +++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/scripts/python/changemouse.py b/src/scripts/python/changemouse.py index 2f18a7c..ec4eedc 100755 --- a/src/scripts/python/changemouse.py +++ b/src/scripts/python/changemouse.py @@ -2,15 +2,15 @@ """Change mouse script -Whenever a mouse is changed to the system, this script will be executed. These +Whenever a mouse is changed to the system, this script will be executed. These are the rules: - + All regular mouses should connect to the "Virtual core pointer" of the + + All regular mouses should connect to the "Virtual core pointer" of the system + Touchpanel should be connected to the master "touch-pointer" NOTE: master "touch-pointer" should be available on the event of adding the mouse. This could be reached by adding this master to the system on boot. - + Steps: + List all pointer as we don't know what pointer is added to the system + group them by their usage by name rules (As if the pointer is eGalax @@ -32,7 +32,7 @@ if __name__ == "__main__": pointers = xup.get_pointers() # TODO filter functionality inside utilities core_master = list(filter(lambda x: "Virtual core" in x.name, pointers))[0] - for pointer in filter(lambda x: not x.slave, pointers): + for pointer in filter(lambda x: x.slave, pointers): if not "eGalax" in pointer.name: com.reattach(pointer.id, core_master.id) else: diff --git a/src/scripts/python/xutil/pointer.py b/src/scripts/python/xutil/pointer.py index 8330b7a..a6afbe1 100644 --- a/src/scripts/python/xutil/pointer.py +++ b/src/scripts/python/xutil/pointer.py @@ -37,6 +37,10 @@ class Pointer(XInput): def __init__(self, name, id, is_master: bool) -> None: super().__init__(name, id, is_master) + @property + def slave(self): + return not self.is_master + def get_short_pointer(id) -> Pointer: """Generates Pointer object corresponding to id (short attrs) @@ -57,7 +61,7 @@ def get_short_pointer(id) -> Pointer: is_master = "master" in props return Pointer( name.strip(), - props.split(maxsplit=1), + props.split(maxsplit=1)[0], is_master) else: raise TypeError(f'id[{id}] is not a pointer id') From d99c9d3d6a47a5258e05df79205bc044f63a3391 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 27 Feb 2023 13:43:57 +0330 Subject: [PATCH 15/45] Add mouse (hid) bind/unbind rule --- src/rules/90-hid-mouse.rules | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/rules/90-hid-mouse.rules diff --git a/src/rules/90-hid-mouse.rules b/src/rules/90-hid-mouse.rules new file mode 100644 index 0000000..b392d93 --- /dev/null +++ b/src/rules/90-hid-mouse.rules @@ -0,0 +1,11 @@ +# Following rule will take affect after binding/unbinding action of HID devices +# which is expected. These rules only take effect one time for each mouse +# insertion, removal. Actually, usb events are triggered when usb and +# usb interface are binding but hid is more specific and happen once for each +# action. change action is not used because +SUBSYSTEM=="hid", ACTION=="bind", RUN+="/usr/local/bin/changemouse.py" + +# the following line may not be needed as when we unplug a mouse it would +# automatically remove devices from the list and they need no furthur +# configuration +# SUBSYSTEM=="hid", ACTION=="unbind", RUN+="/usr/local/bin/changemouse.py" \ No newline at end of file From 14c0b143fa656e3ac8095941875072a01cb1a69b Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 4 Mar 2023 14:08:12 +0330 Subject: [PATCH 16/45] Add randr module This module is meant to provide a wrapper around xrandr. + Pos enum added which is position of screens - commandline options should be applied to it. --- src/scripts/python/xutil/randr.py | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/scripts/python/xutil/randr.py diff --git a/src/scripts/python/xutil/randr.py b/src/scripts/python/xutil/randr.py new file mode 100644 index 0000000..1d2637f --- /dev/null +++ b/src/scripts/python/xutil/randr.py @@ -0,0 +1,36 @@ + +"""RandR + +Author: Ali Hatami Tajik [hatam](mailto:a.hatam008@gmail.com) +Date: 2023 Mar 04 + +--- +This module provides a wrapper utility around xrandr. + +Classes: + Mode + Setting + Screen + Dir + Pos + + +Utilities: + get_screens + +""" + +from enum import Enum +from dataclasses import dataclass + + +class Pos(Enum): + """Position types in xrandr + + Position the output relative to the position of another output. + """ + LEFT_OF = 0, + RIGHT_OF = 1, + ABOVE = 2, + BELOW = 3, + SAME_AS = 4 From 86c1235598c4fd263072744410b8b6f4831a03a3 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 4 Mar 2023 14:23:29 +0330 Subject: [PATCH 17/45] Add rotation and reflection enums reflection and rotation are added from man page --- src/scripts/python/xutil/randr.py | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/scripts/python/xutil/randr.py b/src/scripts/python/xutil/randr.py index 1d2637f..b508a5b 100644 --- a/src/scripts/python/xutil/randr.py +++ b/src/scripts/python/xutil/randr.py @@ -24,6 +24,11 @@ from enum import Enum from dataclasses import dataclass +# TODO: Option class which can be applied by get_args method +# TODO: Screen-related option class ~ +# TODO: abs position + + class Pos(Enum): """Position types in xrandr @@ -34,3 +39,35 @@ class Pos(Enum): ABOVE = 2, BELOW = 3, SAME_AS = 4 + + +class RotationDir(Enum): + """Rotation direction + + This causes the output contents to be rotated in the specified direction. + """ + NORMAL = 0, + LEFT = 1, + RIGHT = 2, + INVERTED = 3 + + +class ReflectDir(Enum): + """Reflection direction + + This causes the output contents to be reflected across the specified axes. + """ + NORMAL = 0, + X = 1, + Y = 2, + XY = 3 + + +@dataclass +class Setting: + resolution = None, + is_primary = False, + is_enabeled = True, + rotation = None + position = None + reflection = None From 90eb2582c6fb2529de10db4fce13afdac7c9b422 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 09:07:11 +0330 Subject: [PATCH 18/45] Stage changes At this point I found out that there is an xlib implementation in python and there is no need fo xutil. So, I stages these changes and I'm going to delete/highly modify those utility files. --- setup.sh | 0 src/scripts/python/changemouse.py | 5 ++--- src/scripts/python/setupmonitor.py | 34 ++++++++++++++++++++++++++++++ src/scripts/python/xutil/randr.py | 34 ++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 setup.sh mode change 100755 => 100644 src/scripts/python/changemouse.py create mode 100644 src/scripts/python/setupmonitor.py diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..e69de29 diff --git a/src/scripts/python/changemouse.py b/src/scripts/python/changemouse.py old mode 100755 new mode 100644 index ec4eedc..0df91d7 --- a/src/scripts/python/changemouse.py +++ b/src/scripts/python/changemouse.py @@ -8,8 +8,7 @@ are the rules: system + Touchpanel should be connected to the master "touch-pointer" -NOTE: master "touch-pointer" should be available on the event of adding the - mouse. This could be reached by adding this master to the system on boot. +NOTE: if master `touch` is not present in the system, Steps: + List all pointer as we don't know what pointer is added to the system @@ -36,5 +35,5 @@ if __name__ == "__main__": if not "eGalax" in pointer.name: com.reattach(pointer.id, core_master.id) else: - # TODO: add touchpannel support + pass diff --git a/src/scripts/python/setupmonitor.py b/src/scripts/python/setupmonitor.py new file mode 100644 index 0000000..ba3e849 --- /dev/null +++ b/src/scripts/python/setupmonitor.py @@ -0,0 +1,34 @@ + +"""Setup Monitor Script + +Author: Ali Hatami Tajik [hatam](mailto:a.hatam008@gmail.com) +Date: 2023 Mar 04 + + This script should be used whenever a change happen in the rdm system, I + guess! But as I investigate a rdm change event will happen many time in case + of addition or removal of a monitor auto configuration is done by the + org.mate.SettingsDaemon.plugins.xrandr deamon. I'll searching for a way to + change that event to run our specified script. + + + This script will do the following: + 1. List currently available monitors + 2. Map those to config file + 3. Replace default values for missing configs + 4. Handle missing touch-screen/main monitor + NOTE: In general we use at least two monitoers with our system. + One is for touch screen and one is for regular screen. In + case of one missing a callback is run to handle that + occurance (an event may send to the software or a temp log + file may be updated so the software adjust ifself + correspondigly). + + Config files are in JSON format and spesify screen output name and their + mode (xrandr resolution and stuff may be added but for now --auto/--prefered + option is used). The format is: + +""" + + +if __name__ == "__main__": + pass diff --git a/src/scripts/python/xutil/randr.py b/src/scripts/python/xutil/randr.py index b508a5b..5eebf9c 100644 --- a/src/scripts/python/xutil/randr.py +++ b/src/scripts/python/xutil/randr.py @@ -22,6 +22,7 @@ Utilities: from enum import Enum from dataclasses import dataclass +from typing import List # TODO: Option class which can be applied by get_args method @@ -65,9 +66,42 @@ class ReflectDir(Enum): @dataclass class Setting: + """Settings of a screen + + This data struct will be used as the config of each screen. Note that + default screen cannot be use + """ resolution = None, is_primary = False, is_enabeled = True, rotation = None position = None reflection = None + + +@dataclass +class Mode: + """Mode + + Mode of the screen including width, height, refresh rate(s) + """ + height: int = 0 + width: int = 0 + frequency: List[int] = [] + + +class Screen: + """Screen class + + This class will hold screen properties and methods related to the screens. + + At the time it will use xrandr (and not the verbose mode) to list the + screens and modes. + """ + + +class Monitor: + """Monitor Class + + List Monitor Outputs and their states + """ \ No newline at end of file From 5d15d79f1b4823b55e272a3c9ed5ae13c7daa4ff Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 12:41:34 +0330 Subject: [PATCH 19/45] Add egalax utility --- src/scripts/python/xutil/egalax.py | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/scripts/python/xutil/egalax.py diff --git a/src/scripts/python/xutil/egalax.py b/src/scripts/python/xutil/egalax.py new file mode 100644 index 0000000..2a29c0b --- /dev/null +++ b/src/scripts/python/xutil/egalax.py @@ -0,0 +1,39 @@ +"""eGalax + +This module is responsible for detecting the touchpanel. It would detect the +touchpannel's drm output and its overal status. +""" + +from pathlib import Path + +VENDOR_ID = "0EEF" +DEVICE_ID = "C000" + +def get_egalax_path() -> Path: + """Get device path + + This function will return the path of the HID device related to the pannel. + NOTE that it is not the path of the EDID but it can be extracted from it. + + Returns: + Path: Path of the eGalax hid device OR None if device is not ceonnected + """ + query = '*' + VENDOR_ID + ':' + DEVICE_ID + '*' + devices = list(Path('/sys/devices').rglob(query)) + if devices: + return devices[0] + else: + return None + + +def is_egalax_connected() -> bool: + """Checks if device is connected + + avaiability of the device is checked by existing a path of the device in the + /sys/devices directory. + + Returns: + bool: True if device is connected + """ + devpath = get_egalax_path() + return bool(devpath) \ No newline at end of file From 611b0da8e665da805c9f03f57c6b239d76db8cd6 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 12:45:13 +0330 Subject: [PATCH 20/45] Move files from xutil to util --- src/scripts/python/changemouse.py | 14 ++++++++++---- src/scripts/python/{xutil => util}/common.py | 17 +++++++++++++++++ src/scripts/python/{xutil => util}/egalax.py | 0 src/scripts/python/{xutil => util}/pointer.py | 9 +++++++++ src/scripts/python/{xutil => util}/randr.py | 2 +- 5 files changed, 37 insertions(+), 5 deletions(-) rename src/scripts/python/{xutil => util}/common.py (81%) rename src/scripts/python/{xutil => util}/egalax.py (100%) rename src/scripts/python/{xutil => util}/pointer.py (93%) rename src/scripts/python/{xutil => util}/randr.py (98%) diff --git a/src/scripts/python/changemouse.py b/src/scripts/python/changemouse.py index 0df91d7..0874671 100644 --- a/src/scripts/python/changemouse.py +++ b/src/scripts/python/changemouse.py @@ -8,8 +8,13 @@ are the rules: system + Touchpanel should be connected to the master "touch-pointer" -NOTE: if master `touch` is not present in the system, - +NOTE: if master `touch` is not present in the system, the script will create a + master `touch` itself and hides the pointer. eGalax device input will be + attached to this master. TODO: make master's cursor invisible. + + Currently we'll use xinput command-line application, but, It is possible + to write a specified c program that uses Xlib efficiently. + Steps: + List all pointer as we don't know what pointer is added to the system + group them by their usage by name rules (As if the pointer is eGalax @@ -23,14 +28,15 @@ NOTE: In case of psyco adds and removes mouses with intervals smaller than run only one instance of this code is running at a moment so no conflicts occur. """ -import xutil.pointer as xup -import xutil.common as com +import util.pointer as xup +import util.common as com if __name__ == "__main__": # If we didn't import the script as utility pointers = xup.get_pointers() # TODO filter functionality inside utilities core_master = list(filter(lambda x: "Virtual core" in x.name, pointers))[0] + for pointer in filter(lambda x: x.slave, pointers): if not "eGalax" in pointer.name: com.reattach(pointer.id, core_master.id) diff --git a/src/scripts/python/xutil/common.py b/src/scripts/python/util/common.py similarity index 81% rename from src/scripts/python/xutil/common.py rename to src/scripts/python/util/common.py index c7c6a86..dee5673 100644 --- a/src/scripts/python/xutil/common.py +++ b/src/scripts/python/util/common.py @@ -54,3 +54,20 @@ def get_ids() -> List[int]: completed = subprocess.run( ['xinput', '--list', '--id-only'], capture_output=True) return list(map(int, completed.stdout.decode(ENCODING).split())) + + +def create_master(name: str = 'touch'): + """Creates master with specified name + + Args: + name (str, optional): name of the master. Defaults to 'touch'. + """ + completed = subprocess.run( + ['xinput create-master', name] + ) + + return completed.returncode + + +def map_to_output(output, device_id): + pass \ No newline at end of file diff --git a/src/scripts/python/xutil/egalax.py b/src/scripts/python/util/egalax.py similarity index 100% rename from src/scripts/python/xutil/egalax.py rename to src/scripts/python/util/egalax.py diff --git a/src/scripts/python/xutil/pointer.py b/src/scripts/python/util/pointer.py similarity index 93% rename from src/scripts/python/xutil/pointer.py rename to src/scripts/python/util/pointer.py index a6afbe1..41fb97f 100644 --- a/src/scripts/python/xutil/pointer.py +++ b/src/scripts/python/util/pointer.py @@ -95,3 +95,12 @@ def get_pointers(is_short=True): else: pass return pointers + + +def get_touch_master(pointers): + """returns Pointer of the master touch pointer + + Args: + pointers (List[Pointes]): list of pointers queried + """ + pass diff --git a/src/scripts/python/xutil/randr.py b/src/scripts/python/util/randr.py similarity index 98% rename from src/scripts/python/xutil/randr.py rename to src/scripts/python/util/randr.py index 5eebf9c..82f6fae 100644 --- a/src/scripts/python/xutil/randr.py +++ b/src/scripts/python/util/randr.py @@ -2,7 +2,7 @@ """RandR Author: Ali Hatami Tajik [hatam](mailto:a.hatam008@gmail.com) -Date: 2023 Mar 04 +Creation Date: 2023 Mar 04 --- This module provides a wrapper utility around xrandr. From e300bd486bab50af88348643f2a4dc97d9fc5ee2 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 12:48:39 +0330 Subject: [PATCH 21/45] Rename util.common to util.x --- src/scripts/python/changemouse.py | 2 +- src/scripts/python/util/pointer.py | 6 +++--- src/scripts/python/util/{common.py => x.py} | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename src/scripts/python/util/{common.py => x.py} (100%) diff --git a/src/scripts/python/changemouse.py b/src/scripts/python/changemouse.py index 0874671..db5494d 100644 --- a/src/scripts/python/changemouse.py +++ b/src/scripts/python/changemouse.py @@ -29,7 +29,7 @@ NOTE: In case of psyco adds and removes mouses with intervals smaller than run """ import util.pointer as xup -import util.common as com +import util.x as com if __name__ == "__main__": # If we didn't import the script as utility diff --git a/src/scripts/python/util/pointer.py b/src/scripts/python/util/pointer.py index 41fb97f..364e515 100644 --- a/src/scripts/python/util/pointer.py +++ b/src/scripts/python/util/pointer.py @@ -1,4 +1,4 @@ -import xutil.common as com +import x as xutil import re @@ -55,7 +55,7 @@ def get_short_pointer(id) -> Pointer: ValueError: if id is not reistered with xinput ValueError: if id is not a pointer id """ - desc = com.get_list_short(id) + desc = xutil.get_list_short(id) name, props = desc.rsplit('id=', 1) if "pointer" in props: is_master = "master" in props @@ -81,7 +81,7 @@ def get_pointers(is_short=True): to the system for processing them individually and per-case). """ pointers = [] - ids = com.get_ids() + ids = xutil.get_ids() for id in ids: if is_short: try: diff --git a/src/scripts/python/util/common.py b/src/scripts/python/util/x.py similarity index 100% rename from src/scripts/python/util/common.py rename to src/scripts/python/util/x.py From f85227f9f20290968c87a4610d6b0b0a943578d3 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 13:41:40 +0330 Subject: [PATCH 22/45] Add max_match This function will be used to find eGalax drm device. --- src/scripts/python/util/common.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/scripts/python/util/common.py diff --git a/src/scripts/python/util/common.py b/src/scripts/python/util/common.py new file mode 100644 index 0000000..a66bb6f --- /dev/null +++ b/src/scripts/python/util/common.py @@ -0,0 +1,30 @@ +"""Common Utilities""" + +def max_match(a: str, b: str) -> str: + """Maximum matching of intersection of pair of string + + This function will return the intersection of two strings from the start. + + Example: + >>> a = "/sys/devices/folan/bahman" + >>> b = "/sys/devices/fol/bahman" + >>> max_match(a,b) + '/sys/dedices/fol' + + Args: + a (str): firsrt string + b (str): second string + + Returns: + str: intersection of two strings + """ + i = 0 + + if len(b) < len(a): + a, b = b, a + + for c in a: + if b[i] == c: + i += 1 + else: + return a[0:i] From f114fe248b40f586c700ef7ad9189cbfaace90db Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 13:52:05 +0330 Subject: [PATCH 23/45] Reformat python scripts --- src/scripts/python/changemouse.py | 3 +-- src/scripts/python/setupmonitor.py | 1 - src/scripts/python/udevhandle.py | 7 +++-- src/scripts/python/util/common.py | 7 ++--- src/scripts/python/util/egalax.py | 11 ++++---- src/scripts/python/util/pointer.py | 19 ++++++-------- src/scripts/python/util/randr.py | 42 ++++++++++++++++-------------- src/scripts/python/util/x.py | 28 +++++++++++--------- 8 files changed, 61 insertions(+), 57 deletions(-) diff --git a/src/scripts/python/changemouse.py b/src/scripts/python/changemouse.py index db5494d..f10447d 100644 --- a/src/scripts/python/changemouse.py +++ b/src/scripts/python/changemouse.py @@ -36,10 +36,9 @@ if __name__ == "__main__": pointers = xup.get_pointers() # TODO filter functionality inside utilities core_master = list(filter(lambda x: "Virtual core" in x.name, pointers))[0] - + for pointer in filter(lambda x: x.slave, pointers): if not "eGalax" in pointer.name: com.reattach(pointer.id, core_master.id) else: - pass diff --git a/src/scripts/python/setupmonitor.py b/src/scripts/python/setupmonitor.py index ba3e849..aa43d9e 100644 --- a/src/scripts/python/setupmonitor.py +++ b/src/scripts/python/setupmonitor.py @@ -1,4 +1,3 @@ - """Setup Monitor Script Author: Ali Hatami Tajik [hatam](mailto:a.hatam008@gmail.com) diff --git a/src/scripts/python/udevhandle.py b/src/scripts/python/udevhandle.py index 5891dac..1e79eb3 100644 --- a/src/scripts/python/udevhandle.py +++ b/src/scripts/python/udevhandle.py @@ -57,9 +57,8 @@ class MouseHandler(Handler): or we can invoke xinput binaries via python itself. a bash script solution would be benefitial since it can used as utility). """ - # TODO: use somthing that only captures - super().__init__('usb') - + # TODO: use somthing that only captures + super().__init__("usb") def callback(self, device): - print(device.action) \ No newline at end of file + print(device.action) diff --git a/src/scripts/python/util/common.py b/src/scripts/python/util/common.py index a66bb6f..6d40ee7 100644 --- a/src/scripts/python/util/common.py +++ b/src/scripts/python/util/common.py @@ -1,10 +1,11 @@ """Common Utilities""" + def max_match(a: str, b: str) -> str: """Maximum matching of intersection of pair of string This function will return the intersection of two strings from the start. - + Example: >>> a = "/sys/devices/folan/bahman" >>> b = "/sys/devices/fol/bahman" @@ -19,10 +20,10 @@ def max_match(a: str, b: str) -> str: str: intersection of two strings """ i = 0 - + if len(b) < len(a): a, b = b, a - + for c in a: if b[i] == c: i += 1 diff --git a/src/scripts/python/util/egalax.py b/src/scripts/python/util/egalax.py index 2a29c0b..074735a 100644 --- a/src/scripts/python/util/egalax.py +++ b/src/scripts/python/util/egalax.py @@ -9,22 +9,23 @@ from pathlib import Path VENDOR_ID = "0EEF" DEVICE_ID = "C000" + def get_egalax_path() -> Path: """Get device path - + This function will return the path of the HID device related to the pannel. NOTE that it is not the path of the EDID but it can be extracted from it. Returns: Path: Path of the eGalax hid device OR None if device is not ceonnected """ - query = '*' + VENDOR_ID + ':' + DEVICE_ID + '*' - devices = list(Path('/sys/devices').rglob(query)) + query = "*" + VENDOR_ID + ":" + DEVICE_ID + "*" + devices = list(Path("/sys/devices").rglob(query)) if devices: return devices[0] else: return None - + def is_egalax_connected() -> bool: """Checks if device is connected @@ -36,4 +37,4 @@ def is_egalax_connected() -> bool: bool: True if device is connected """ devpath = get_egalax_path() - return bool(devpath) \ No newline at end of file + return bool(devpath) diff --git a/src/scripts/python/util/pointer.py b/src/scripts/python/util/pointer.py index 364e515..d645a4b 100644 --- a/src/scripts/python/util/pointer.py +++ b/src/scripts/python/util/pointer.py @@ -17,7 +17,7 @@ class XInput: Args: name (str): name of the input. No processing is done on the name id (int): id of the input - is_master (bool): master status of the input device + is_master (bool): master status of the input device """ self.name = name self.id = id @@ -43,7 +43,7 @@ class Pointer(XInput): def get_short_pointer(id) -> Pointer: - """Generates Pointer object corresponding to id (short attrs) + """Generates Pointer object corresponding to id (short attrs) Args: id (int): pointer id @@ -56,15 +56,12 @@ def get_short_pointer(id) -> Pointer: ValueError: if id is not a pointer id """ desc = xutil.get_list_short(id) - name, props = desc.rsplit('id=', 1) + name, props = desc.rsplit("id=", 1) if "pointer" in props: is_master = "master" in props - return Pointer( - name.strip(), - props.split(maxsplit=1)[0], - is_master) + return Pointer(name.strip(), props.split(maxsplit=1)[0], is_master) else: - raise TypeError(f'id[{id}] is not a pointer id') + raise TypeError(f"id[{id}] is not a pointer id") def get_pointers(is_short=True): @@ -99,8 +96,8 @@ def get_pointers(is_short=True): def get_touch_master(pointers): """returns Pointer of the master touch pointer - + Args: - pointers (List[Pointes]): list of pointers queried + pointers (List[Pointes]): list of pointers queried """ - pass + pass diff --git a/src/scripts/python/util/randr.py b/src/scripts/python/util/randr.py index 82f6fae..37fe1da 100644 --- a/src/scripts/python/util/randr.py +++ b/src/scripts/python/util/randr.py @@ -1,4 +1,3 @@ - """RandR Author: Ali Hatami Tajik [hatam](mailto:a.hatam008@gmail.com) @@ -35,10 +34,11 @@ class Pos(Enum): Position the output relative to the position of another output. """ - LEFT_OF = 0, - RIGHT_OF = 1, - ABOVE = 2, - BELOW = 3, + + LEFT_OF = (0,) + RIGHT_OF = (1,) + ABOVE = (2,) + BELOW = (3,) SAME_AS = 4 @@ -47,9 +47,10 @@ class RotationDir(Enum): This causes the output contents to be rotated in the specified direction. """ - NORMAL = 0, - LEFT = 1, - RIGHT = 2, + + NORMAL = (0,) + LEFT = (1,) + RIGHT = (2,) INVERTED = 3 @@ -58,9 +59,10 @@ class ReflectDir(Enum): This causes the output contents to be reflected across the specified axes. """ - NORMAL = 0, - X = 1, - Y = 2, + + NORMAL = (0,) + X = (1,) + Y = (2,) XY = 3 @@ -71,9 +73,10 @@ class Setting: This data struct will be used as the config of each screen. Note that default screen cannot be use """ - resolution = None, - is_primary = False, - is_enabeled = True, + + resolution = (None,) + is_primary = (False,) + is_enabeled = (True,) rotation = None position = None reflection = None @@ -82,9 +85,10 @@ class Setting: @dataclass class Mode: """Mode - + Mode of the screen including width, height, refresh rate(s) """ + height: int = 0 width: int = 0 frequency: List[int] = [] @@ -94,14 +98,14 @@ class Screen: """Screen class This class will hold screen properties and methods related to the screens. - + At the time it will use xrandr (and not the verbose mode) to list the screens and modes. """ - + class Monitor: """Monitor Class - + List Monitor Outputs and their states - """ \ No newline at end of file + """ diff --git a/src/scripts/python/util/x.py b/src/scripts/python/util/x.py index dee5673..a9cdba7 100644 --- a/src/scripts/python/util/x.py +++ b/src/scripts/python/util/x.py @@ -1,8 +1,9 @@ import subprocess from typing import List from multipledispatch import dispatch +from pathlib import Path -ENCODING = 'utf-8' +ENCODING = "utf-8" @dispatch() @@ -10,7 +11,8 @@ def get_list_short(): """Returns string output of the `xinput --list --short` command encoded as UTF-8""" completed = subprocess.run( - ['xinput', '--list', '--short'], capture_output=True) + ["xinput", "--list", "--short"], capture_output=True + ) return completed.stdout.decode(ENCODING) @@ -25,12 +27,13 @@ def get_list_short(id): ValueError: in case of id not found in devices """ completed = subprocess.run( - ['xinput', '--list', '--short', str(id)], capture_output=True) + ["xinput", "--list", "--short", str(id)], capture_output=True + ) if completed.returncode == 0: return completed.stdout.decode(ENCODING) else: - ValueError(f'id[{id}] is not registered') + ValueError(f"id[{id}] is not registered") def reattach(id, master): @@ -44,7 +47,8 @@ def reattach(id, master): id is not valid, xinput will not do anything and nothing bad will happen :) """ completed = subprocess.run( - ['xinput', '--reattach', str(id), str(master)], capture_output=True) + ["xinput", "--reattach", str(id), str(master)], capture_output=True + ) return completed.returncode @@ -52,22 +56,22 @@ def reattach(id, master): def get_ids() -> List[int]: """returns list of ids registered in xinput""" completed = subprocess.run( - ['xinput', '--list', '--id-only'], capture_output=True) + ["xinput", "--list", "--id-only"], capture_output=True + ) return list(map(int, completed.stdout.decode(ENCODING).split())) -def create_master(name: str = 'touch'): +def create_master(name: str = "touch"): """Creates master with specified name Args: name (str, optional): name of the master. Defaults to 'touch'. """ - completed = subprocess.run( - ['xinput create-master', name] - ) - + completed = subprocess.run(["xinput create-master", name]) + return completed.returncode def map_to_output(output, device_id): - pass \ No newline at end of file + # TODO + pass From 0ab92a178ca1bacb0422e3c5193687f0ee51a87c Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 13:52:36 +0330 Subject: [PATCH 24/45] Add get_edid_dev_path function --- src/scripts/python/util/x.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/scripts/python/util/x.py b/src/scripts/python/util/x.py index a9cdba7..d07f72c 100644 --- a/src/scripts/python/util/x.py +++ b/src/scripts/python/util/x.py @@ -75,3 +75,11 @@ def create_master(name: str = "touch"): def map_to_output(output, device_id): # TODO pass + + +def get_edid_dev_path(): + """returns iterator of pathes of devices with edid + + devices which has EDID are monitors. + """ + return Path("/sys/devices").rglob("edid") From 57b7774ffb4332be9a9637147893ae8f2376abde Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 14:28:33 +0330 Subject: [PATCH 25/45] Add get_egalax_edid_path --- src/scripts/python/util/common.py | 6 +++++- src/scripts/python/util/egalax.py | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/scripts/python/util/common.py b/src/scripts/python/util/common.py index 6d40ee7..6e2bd91 100644 --- a/src/scripts/python/util/common.py +++ b/src/scripts/python/util/common.py @@ -17,10 +17,14 @@ def max_match(a: str, b: str) -> str: b (str): second string Returns: - str: intersection of two strings + str: intersection of two strings OR None if one or both strings are + empty or None """ i = 0 + if not a or not b: + return None + if len(b) < len(a): a, b = b, a diff --git a/src/scripts/python/util/egalax.py b/src/scripts/python/util/egalax.py index 074735a..9a9f31d 100644 --- a/src/scripts/python/util/egalax.py +++ b/src/scripts/python/util/egalax.py @@ -5,6 +5,8 @@ touchpannel's drm output and its overal status. """ from pathlib import Path +from x import get_edid_dev_path +from common import max_match VENDOR_ID = "0EEF" DEVICE_ID = "C000" @@ -38,3 +40,28 @@ def is_egalax_connected() -> bool: """ devpath = get_egalax_path() return bool(devpath) + + +def get_egalax_edid_path() -> Path: + """return EDID path of touchpannel rdm + + This function will find intersection of the edid pathes and eGalax hid + device and if this intersection and returns the maximum match. + + Runtime: 160ms on average -> Not efficient + + Returns: + Path: edid path of eGalax OR None if not found or device is'nt connected + """ + egalax_dev = get_egalax_path() + if not egalax_dev: + return None + max_dir = "/sys/devices" + max_path = None + for path in get_edid_dev_path(): + base_dir = max_match(str(path), str(egalax_dev)) + if len(max_dir) < len(base_dir): + max_dir = base_dir + max_path = path + # TODO add sanity check (both edid and VENDOR:DEVICE are in that base) + return max_path From 843835e980c2466f324b5368e847c12aa11a1c06 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 14:32:31 +0330 Subject: [PATCH 26/45] Add get_egalax_drm_pure_name --- src/scripts/python/util/egalax.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/scripts/python/util/egalax.py b/src/scripts/python/util/egalax.py index 9a9f31d..1cf43aa 100644 --- a/src/scripts/python/util/egalax.py +++ b/src/scripts/python/util/egalax.py @@ -65,3 +65,16 @@ def get_egalax_edid_path() -> Path: max_path = path # TODO add sanity check (both edid and VENDOR:DEVICE are in that base) return max_path + + +def get_egalax_drm_pure_name() -> str: + """Extract DRM name form edid path + + Returns: + str: pure drm name OR none if device is not found + """ + edid_path = get_egalax_edid_path() + if edid_path: + return str(edid_path.parent.stem) + else: + return None From d3072117e4e30d348b4e830384f6090dfc618cb6 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 15:06:31 +0330 Subject: [PATCH 27/45] init install script --- setup.sh | 42 +++++++ src/scripts/utils/progressbar.sh | 205 +++++++++++++++++++++++++++++++ src/scripts/utils/source.sh | 6 + 3 files changed, 253 insertions(+) mode change 100644 => 100755 setup.sh create mode 100644 src/scripts/utils/progressbar.sh diff --git a/setup.sh b/setup.sh old mode 100644 new mode 100755 index e69de29..a5fbdc8 --- a/setup.sh +++ b/setup.sh @@ -0,0 +1,42 @@ +#!/bin/env bash + +source ./src/scripts/utils/source.sh +source ./src/scripts/utils/progressbar.sh + +while getopts 'v' OPTION; do + case "$OPTION" in + v) + _V=1 + ;; + ?) + echo "usage: ./setup.sh [-v]" >&2 + exit 1 + ;; + esac +done + +draw_progress_bar 0 +log '.: Setting up sono-os v0.1.0 :.' + +sleep 1 +draw_progress_bar 5 +log 'Installing scripts ...' +# TODO +sleep 1 + +draw_progress_bar 45 +log 'Installing config files ...' +# TODO +sleep 1 + +draw_progress_bar 65 +log 'Configuring Logger ...' +# TODO +sleep 1 + +draw_progress_bar 85 +log 'Copying rules to udev ...' +# TODO +sleep 1 + +draw_progress_bar 100 \ No newline at end of file diff --git a/src/scripts/utils/progressbar.sh b/src/scripts/utils/progressbar.sh new file mode 100644 index 0000000..2a75440 --- /dev/null +++ b/src/scripts/utils/progressbar.sh @@ -0,0 +1,205 @@ +#!/bin/bash +# https://github.com/pollev/bash_progress_bar - See license at end of file + +# Usage: +# Source this script +# enable_trapping <- optional to clean up properly if user presses ctrl-c +# setup_scroll_area <- create empty progress bar +# draw_progress_bar 10 <- advance progress bar +# draw_progress_bar 40 <- advance progress bar +# block_progress_bar 45 <- turns the progress bar yellow to indicate some action is requested from the user +# draw_progress_bar 90 <- advance progress bar +# destroy_scroll_area <- remove progress bar + +# Constants +CODE_SAVE_CURSOR="\033[s" +CODE_RESTORE_CURSOR="\033[u" +CODE_CURSOR_IN_SCROLL_AREA="\033[1A" +COLOR_FG="\e[30m" +COLOR_BG="\e[42m" +COLOR_BG_BLOCKED="\e[43m" +RESTORE_FG="\e[39m" +RESTORE_BG="\e[49m" + +# Variables +PROGRESS_BLOCKED="false" +TRAPPING_ENABLED="false" +TRAP_SET="false" + +CURRENT_NR_LINES=0 + +setup_scroll_area() { + # If trapping is enabled, we will want to activate it whenever we setup the scroll area and remove it when we break the scroll area + if [ "$TRAPPING_ENABLED" = "true" ]; then + trap_on_interrupt + fi + + lines=$(tput lines) + CURRENT_NR_LINES=$lines + let lines=$lines-1 + # Scroll down a bit to avoid visual glitch when the screen area shrinks by one row + log -en "\n" + + # Save cursor + log -en "$CODE_SAVE_CURSOR" + # Set scroll region (this will place the cursor in the top left) + log -en "\033[0;${lines}r" + + # Restore cursor but ensure its inside the scrolling area + log -en "$CODE_RESTORE_CURSOR" + log -en "$CODE_CURSOR_IN_SCROLL_AREA" + + # Start empty progress bar + draw_progress_bar 0 +} + +destroy_scroll_area() { + lines=$(tput lines) + # Save cursor + log -en "$CODE_SAVE_CURSOR" + # Set scroll region (this will place the cursor in the top left) + log -en "\033[0;${lines}r" + + # Restore cursor but ensure its inside the scrolling area + log -en "$CODE_RESTORE_CURSOR" + log -en "$CODE_CURSOR_IN_SCROLL_AREA" + + # We are done so clear the scroll bar + clear_progress_bar + + # Scroll down a bit to avoid visual glitch when the screen area grows by one row + log -en "\n\n" + + # Once the scroll area is cleared, we want to remove any trap previously set. Otherwise, ctrl+c will exit our shell + if [ "$TRAP_SET" = "true" ]; then + trap - INT + fi +} + +draw_progress_bar() { + percentage=$1 + lines=$(tput lines) + let lines=$lines + + # Check if the window has been resized. If so, reset the scroll area + if [ "$lines" -ne "$CURRENT_NR_LINES" ]; then + setup_scroll_area + fi + + # Save cursor + log -en "$CODE_SAVE_CURSOR" + + # Move cursor position to last row + log -en "\033[${lines};0f" + + # Clear progress bar + tput el + + # Draw progress bar + PROGRESS_BLOCKED="false" + print_bar_text $percentage + + # Restore cursor position + log -en "$CODE_RESTORE_CURSOR" +} + +block_progress_bar() { + percentage=$1 + lines=$(tput lines) + let lines=$lines + # Save cursor + log -en "$CODE_SAVE_CURSOR" + + # Move cursor position to last row + log -en "\033[${lines};0f" + + # Clear progress bar + tput el + + # Draw progress bar + PROGRESS_BLOCKED="true" + print_bar_text $percentage + + # Restore cursor position + log -en "$CODE_RESTORE_CURSOR" +} + +clear_progress_bar() { + lines=$(tput lines) + let lines=$lines + # Save cursor + log -en "$CODE_SAVE_CURSOR" + + # Move cursor position to last row + log -en "\033[${lines};0f" + + # clear progress bar + tput el + + # Restore cursor position + log -en "$CODE_RESTORE_CURSOR" +} + +print_bar_text() { + local percentage=$1 + local cols=$(tput cols) + let bar_size=$cols-17 + + local color="${COLOR_FG}${COLOR_BG}" + if [ "$PROGRESS_BLOCKED" = "true" ]; then + color="${COLOR_FG}${COLOR_BG_BLOCKED}" + fi + + # Prepare progress bar + let complete_size=($bar_size*$percentage)/100 + let remainder_size=$bar_size-$complete_size + progress_bar=$(log -ne "["; log -en "${color}"; printf_new "#" $complete_size; log -en "${RESTORE_FG}${RESTORE_BG}"; printf_new "." $remainder_size; log -ne "]"); + + # Print progress bar + log -ne " Progress ${percentage}% ${progress_bar}" +} + +enable_trapping() { + TRAPPING_ENABLED="true" +} + +trap_on_interrupt() { + # If this function is called, we setup an interrupt handler to cleanup the progress bar + TRAP_SET="true" + trap cleanup_on_interrupt INT +} + +cleanup_on_interrupt() { + destroy_scroll_area + exit +} + +printf_new() { + str=$1 + num=$2 + v=$(printf "%-${num}s" "$str") + log -ne "${v// /$str}" +} + + +# SPDX-License-Identifier: MIT +# +# Copyright (c) 2018--2020 Polle Vanhoof +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/src/scripts/utils/source.sh b/src/scripts/utils/source.sh index 8a63a7d..1e64e2c 100644 --- a/src/scripts/utils/source.sh +++ b/src/scripts/utils/source.sh @@ -391,4 +391,10 @@ function run_as_root() { fi } +function log () { + if [[ $_V -eq 1 ]]; then + echo "$@" + fi +} + # vim: syntax=sh cc=80 tw=79 ts=4 sw=4 sts=4 et sr \ No newline at end of file From f8898b464f7c891ad355c1bdb769bbd6f868ab71 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 15:09:11 +0330 Subject: [PATCH 28/45] Inhance progressbar --- setup.sh | 4 +++- src/scripts/utils/progressbar.sh | 10 ---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/setup.sh b/setup.sh index a5fbdc8..ac18cdc 100755 --- a/setup.sh +++ b/setup.sh @@ -15,6 +15,7 @@ while getopts 'v' OPTION; do esac done +enable_trapping draw_progress_bar 0 log '.: Setting up sono-os v0.1.0 :.' @@ -39,4 +40,5 @@ log 'Copying rules to udev ...' # TODO sleep 1 -draw_progress_bar 100 \ No newline at end of file +draw_progress_bar 100 +destroy_scroll_area \ No newline at end of file diff --git a/src/scripts/utils/progressbar.sh b/src/scripts/utils/progressbar.sh index 2a75440..075f33b 100644 --- a/src/scripts/utils/progressbar.sh +++ b/src/scripts/utils/progressbar.sh @@ -1,16 +1,6 @@ #!/bin/bash # https://github.com/pollev/bash_progress_bar - See license at end of file -# Usage: -# Source this script -# enable_trapping <- optional to clean up properly if user presses ctrl-c -# setup_scroll_area <- create empty progress bar -# draw_progress_bar 10 <- advance progress bar -# draw_progress_bar 40 <- advance progress bar -# block_progress_bar 45 <- turns the progress bar yellow to indicate some action is requested from the user -# draw_progress_bar 90 <- advance progress bar -# destroy_scroll_area <- remove progress bar - # Constants CODE_SAVE_CURSOR="\033[s" CODE_RESTORE_CURSOR="\033[u" From 2d83018fcb21e790a3bc03cbdcf68f9156f47fc9 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 15:57:38 +0330 Subject: [PATCH 29/45] Refactor pointer generation --- src/scripts/python/util/pointer.py | 85 ++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/src/scripts/python/util/pointer.py b/src/scripts/python/util/pointer.py index d645a4b..213aee1 100644 --- a/src/scripts/python/util/pointer.py +++ b/src/scripts/python/util/pointer.py @@ -1,5 +1,10 @@ import x as xutil import re +from enum import Enum + + +class XInputState(Enum): + SLAVE, MASTER, FLOATING = range(3) class XInput: @@ -11,7 +16,7 @@ class XInput: is_master (bool): True if device is master """ - def __init__(self, name, id, is_master) -> None: + def __init__(self, name, id, state) -> None: """Initializes the class with name, id and master status Args: @@ -21,7 +26,7 @@ class XInput: """ self.name = name self.id = id - self.is_master = is_master + self.state = state class Pointer(XInput): @@ -34,12 +39,19 @@ class Pointer(XInput): is_master (bool): True if the pointer is a master pointer else False """ - def __init__(self, name, id, is_master: bool) -> None: - super().__init__(name, id, is_master) + def __init__(self, name, id, state) -> None: + super().__init__(name, id, state) @property def slave(self): - return not self.is_master + return self.state == XInputState.SLAVE + + @property + def master(self): + return self.state == XInputState.MASTER + + def floating(self): + return self.state == XInputState.FLOATING def get_short_pointer(id) -> Pointer: @@ -58,31 +70,41 @@ def get_short_pointer(id) -> Pointer: desc = xutil.get_list_short(id) name, props = desc.rsplit("id=", 1) if "pointer" in props: - is_master = "master" in props - return Pointer(name.strip(), props.split(maxsplit=1)[0], is_master) + state = XInputState.FLOATING + if "master" in props: + state = XInputState.MASTER + elif "slave" in props: + state = XInputState.SLAVE + return Pointer(name.strip(), props.split(maxsplit=1)[0], state) else: raise TypeError(f"id[{id}] is not a pointer id") -def get_pointers(is_short=True): - """Wraps pointers in `xinput --list` in Pointer class +def get_ids_iter(): + """xinput id generator - Creation of the pointer is done by getting the list description of - each id. if the is_short arg is True, then short list description will be - used which will provide the class `name`, `is_master` and `id` values. - - Getting this pointers is done by first calling `xinput --list --id-only` to - get ids and then execute `xinput --list {id}` to get the description with - less-complicated output compare to iterating over `xinput --list --short` - line by line (--short option has some special characters that cause overhead - to the system for processing them individually and per-case). + Yields: + int: id of xinput devices """ - pointers = [] ids = xutil.get_ids() for id in ids: + yield id + + +def get_pointer_iter(is_short=True): + """xinput pointers generator + + Args: + is_short (bool, optional): if True generates short type pointers. + Defaults to True. + + Yields: + Pointer: xinput pointers + """ + for id in get_ids_iter(): if is_short: try: - pointers.append(get_short_pointer(id)) + yield get_short_pointer(id) except TypeError as e: # ignore if the id is not pointer pass @@ -90,14 +112,23 @@ def get_pointers(is_short=True): # TODO: logging pass else: - pass - return pointers + pass # TODO -def get_touch_master(pointers): - """returns Pointer of the master touch pointer +def get_pointers(is_short=True): + """Wraps pointers in `xinput --list` in Pointer class - Args: - pointers (List[Pointes]): list of pointers queried + Creation of the pointer is done by getting the list description of + each id. if the is_short arg is True, then short list description will be + used which will provide the class `name`, `is_master` and `id` values. + + Getting this pointers is done by first calling `xinput --list --id-only` to + get ids and then execute `xinput --list {id}` to get the description with + less-complicated output compare to iterating over `xinput --list --short` + line by line (--short option has some special characters that cause overhead + to the system for processing them individually and per-case). """ - pass + pointers = [] + for pointer in get_pointer_iter(is_short): + pointers.append(pointer) + return pointers From f1237d72e1966212e2d832f160babf6e81cfa09c Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 16:19:23 +0330 Subject: [PATCH 30/45] Add get_xi_id_by_name --- src/scripts/python/util/x.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/scripts/python/util/x.py b/src/scripts/python/util/x.py index d07f72c..358e0e2 100644 --- a/src/scripts/python/util/x.py +++ b/src/scripts/python/util/x.py @@ -72,6 +72,22 @@ def create_master(name: str = "touch"): return completed.returncode +def get_xi_id_by_name(name): + """find device id from name + + Args: + name (str): name of the device + """ + completed = subprocess.run( + ["xinput list --id-only", name], capture_output=True + ) + + if completed.returncode == 1: + return -1 + else: + int(completed.stdout.decode(ENCODING)) + + def map_to_output(output, device_id): # TODO pass From 61c1f6aa7e1b0980ab87972a2c2d2978cf982ea3 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 16:20:03 +0330 Subject: [PATCH 31/45] Add get_pointers_categorized This function will filtered pointers for EZ useage in changemouse script --- src/scripts/python/util/pointer.py | 67 ++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/scripts/python/util/pointer.py b/src/scripts/python/util/pointer.py index 213aee1..0947943 100644 --- a/src/scripts/python/util/pointer.py +++ b/src/scripts/python/util/pointer.py @@ -42,6 +42,9 @@ class Pointer(XInput): def __init__(self, name, id, state) -> None: super().__init__(name, id, state) + def __repr__(self) -> str: + return f" Date: Sat, 11 Mar 2023 16:35:02 +0330 Subject: [PATCH 32/45] Fix create-master system error --- src/scripts/python/util/pointer.py | 5 ++--- src/scripts/python/util/x.py | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/scripts/python/util/pointer.py b/src/scripts/python/util/pointer.py index 0947943..fe4a0e7 100644 --- a/src/scripts/python/util/pointer.py +++ b/src/scripts/python/util/pointer.py @@ -1,5 +1,4 @@ -import x as xutil -import re +import util.x as xutil from enum import Enum @@ -43,7 +42,7 @@ class Pointer(XInput): super().__init__(name, id, state) def __repr__(self) -> str: - return f"" @property def slave(self): diff --git a/src/scripts/python/util/x.py b/src/scripts/python/util/x.py index 358e0e2..aa55a01 100644 --- a/src/scripts/python/util/x.py +++ b/src/scripts/python/util/x.py @@ -67,7 +67,7 @@ def create_master(name: str = "touch"): Args: name (str, optional): name of the master. Defaults to 'touch'. """ - completed = subprocess.run(["xinput create-master", name]) + completed = subprocess.run(["xinput", "create-master", name]) return completed.returncode @@ -79,13 +79,13 @@ def get_xi_id_by_name(name): name (str): name of the device """ completed = subprocess.run( - ["xinput list --id-only", name], capture_output=True + ["xinput", "list", "--id-only", name], capture_output=True ) if completed.returncode == 1: - return -1 + return None else: - int(completed.stdout.decode(ENCODING)) + return int(completed.stdout.decode(ENCODING)) def map_to_output(output, device_id): From 50b9200b1718d1587ff56f85bb276980807882a9 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Sat, 11 Mar 2023 16:40:08 +0330 Subject: [PATCH 33/45] Update changemouse script add pointer binding utility to the script. --- src/scripts/python/changemouse.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/scripts/python/changemouse.py b/src/scripts/python/changemouse.py index f10447d..3faa813 100644 --- a/src/scripts/python/changemouse.py +++ b/src/scripts/python/changemouse.py @@ -28,17 +28,16 @@ NOTE: In case of psyco adds and removes mouses with intervals smaller than run only one instance of this code is running at a moment so no conflicts occur. """ -import util.pointer as xup -import util.x as com +import util.pointer as putil +import util.x as xutil if __name__ == "__main__": - # If we didn't import the script as utility - pointers = xup.get_pointers() - # TODO filter functionality inside utilities - core_master = list(filter(lambda x: "Virtual core" in x.name, pointers))[0] - - for pointer in filter(lambda x: x.slave, pointers): - if not "eGalax" in pointer.name: - com.reattach(pointer.id, core_master.id) - else: - pass + """Configure Pointers + + Execution time: 200ms avg -> Tolarable + """ + v_core, touch_master, e_galax, pointers = putil.get_pointers_categorized() + if e_galax: + xutil.reattach(e_galax.id, touch_master.id) + for p in pointers: + xutil.reattach(p.id, v_core.id) From e94fca8ab49b63700fb37ed3416fb8acb901546c Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 13 Mar 2023 13:34:25 +0330 Subject: [PATCH 34/45] Remove unneccessary imports This reduces the overhead of running the script. --- src/scripts/python/changemouse.py | 5 ++++- src/scripts/python/util/pointer.py | 20 +++++++++----------- src/scripts/python/util/x.py | 5 ++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/scripts/python/changemouse.py b/src/scripts/python/changemouse.py index 3faa813..37ab920 100644 --- a/src/scripts/python/changemouse.py +++ b/src/scripts/python/changemouse.py @@ -34,10 +34,13 @@ import util.x as xutil if __name__ == "__main__": """Configure Pointers - Execution time: 200ms avg -> Tolarable + Execution time: 140ms avg -> Tolarable """ v_core, touch_master, e_galax, pointers = putil.get_pointers_categorized() if e_galax: xutil.reattach(e_galax.id, touch_master.id) + else: + # TODO: disable touch? + pass for p in pointers: xutil.reattach(p.id, v_core.id) diff --git a/src/scripts/python/util/pointer.py b/src/scripts/python/util/pointer.py index fe4a0e7..2e6795f 100644 --- a/src/scripts/python/util/pointer.py +++ b/src/scripts/python/util/pointer.py @@ -1,9 +1,7 @@ import util.x as xutil -from enum import Enum - -class XInputState(Enum): - SLAVE, MASTER, FLOATING = range(3) +# Pointer states +SLAVE, MASTER, FLOATING = range(3) class XInput: @@ -46,14 +44,14 @@ class Pointer(XInput): @property def slave(self): - return self.state == XInputState.SLAVE + return self.state == SLAVE @property def master(self): - return self.state == XInputState.MASTER + return self.state == MASTER def floating(self): - return self.state == XInputState.FLOATING + return self.state == FLOATING def get_short_pointer(id) -> Pointer: @@ -69,14 +67,14 @@ def get_short_pointer(id) -> Pointer: ValueError: if id is not reistered with xinput ValueError: if id is not a pointer id """ - desc = xutil.get_list_short(id) + desc = xutil.get_list_short_with(id) name, props = desc.rsplit("id=", 1) if "pointer" in props: - state = XInputState.FLOATING + state = FLOATING if "master" in props: - state = XInputState.MASTER + state = MASTER elif "slave" in props: - state = XInputState.SLAVE + state = SLAVE return Pointer(name.strip(), props.split(maxsplit=1)[0], state) else: raise TypeError(f"id[{id}] is not a pointer id") diff --git a/src/scripts/python/util/x.py b/src/scripts/python/util/x.py index aa55a01..8fd0c6f 100644 --- a/src/scripts/python/util/x.py +++ b/src/scripts/python/util/x.py @@ -1,5 +1,4 @@ import subprocess -from typing import List from multipledispatch import dispatch from pathlib import Path @@ -17,7 +16,7 @@ def get_list_short(): @dispatch(int) -def get_list_short(id): +def get_list_short_with(id): """Short List of the id Args: @@ -53,7 +52,7 @@ def reattach(id, master): return completed.returncode -def get_ids() -> List[int]: +def get_ids(): """returns list of ids registered in xinput""" completed = subprocess.run( ["xinput", "--list", "--id-only"], capture_output=True From a14c9e5cb2a5238413f1d59d67fae201df7df49b Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 13 Mar 2023 15:39:13 +0330 Subject: [PATCH 35/45] Stage chenges of x module --- src/scripts/python/util/x.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/scripts/python/util/x.py b/src/scripts/python/util/x.py index 8fd0c6f..8457433 100644 --- a/src/scripts/python/util/x.py +++ b/src/scripts/python/util/x.py @@ -1,21 +1,27 @@ import subprocess -from multipledispatch import dispatch from pathlib import Path +import os ENCODING = "utf-8" +XINPUT = "/ust/bin/xinput" + + +def exec_xinput(args: list): + args.insert(0, XINPUT) + _read, _write = os.pipe() + write_fd = os.fdopen(_write, "w", 0) + os.read() -@dispatch() def get_list_short(): """Returns string output of the `xinput --list --short` command encoded as UTF-8""" completed = subprocess.run( - ["xinput", "--list", "--short"], capture_output=True + [XINPUT, "--list", "--short"], capture_output=True ) return completed.stdout.decode(ENCODING) -@dispatch(int) def get_list_short_with(id): """Short List of the id @@ -26,7 +32,7 @@ def get_list_short_with(id): ValueError: in case of id not found in devices """ completed = subprocess.run( - ["xinput", "--list", "--short", str(id)], capture_output=True + [XINPUT, "--list", "--short", str(id)], capture_output=True ) if completed.returncode == 0: @@ -46,7 +52,7 @@ def reattach(id, master): id is not valid, xinput will not do anything and nothing bad will happen :) """ completed = subprocess.run( - ["xinput", "--reattach", str(id), str(master)], capture_output=True + [XINPUT, "--reattach", str(id), str(master)], capture_output=True ) return completed.returncode @@ -55,7 +61,7 @@ def reattach(id, master): def get_ids(): """returns list of ids registered in xinput""" completed = subprocess.run( - ["xinput", "--list", "--id-only"], capture_output=True + [XINPUT, "--list", "--id-only"], capture_output=True ) return list(map(int, completed.stdout.decode(ENCODING).split())) @@ -66,7 +72,7 @@ def create_master(name: str = "touch"): Args: name (str, optional): name of the master. Defaults to 'touch'. """ - completed = subprocess.run(["xinput", "create-master", name]) + completed = subprocess.run([XINPUT, "create-master", name]) return completed.returncode @@ -78,7 +84,7 @@ def get_xi_id_by_name(name): name (str): name of the device """ completed = subprocess.run( - ["xinput", "list", "--id-only", name], capture_output=True + [XINPUT, "list", "--id-only", name], capture_output=True ) if completed.returncode == 1: From 93a0a73e907b67ca072a3749d3f291c2c47d9c44 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 13 Mar 2023 15:40:53 +0330 Subject: [PATCH 36/45] Remove multipledispatch module --- src/scripts/python/util/x.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/scripts/python/util/x.py b/src/scripts/python/util/x.py index 8fd0c6f..aef22aa 100644 --- a/src/scripts/python/util/x.py +++ b/src/scripts/python/util/x.py @@ -1,11 +1,9 @@ import subprocess -from multipledispatch import dispatch from pathlib import Path ENCODING = "utf-8" -@dispatch() def get_list_short(): """Returns string output of the `xinput --list --short` command encoded as UTF-8""" @@ -15,7 +13,6 @@ def get_list_short(): return completed.stdout.decode(ENCODING) -@dispatch(int) def get_list_short_with(id): """Short List of the id From 7dda5c15d1f610132e8f8540b5d52b097a266fe5 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 13 Mar 2023 17:11:36 +0330 Subject: [PATCH 37/45] Adds snippet list monitors --- src/scripts/python/setupmonitor.py | 10 ++++++++++ src/scripts/python/util/randr.py | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/scripts/python/setupmonitor.py b/src/scripts/python/setupmonitor.py index aa43d9e..3942701 100644 --- a/src/scripts/python/setupmonitor.py +++ b/src/scripts/python/setupmonitor.py @@ -28,6 +28,16 @@ Date: 2023 Mar 04 """ +import Xlib.display + +display = Xlib.display.Display() +root = display.screen().root +for m in root.xrandr_get_monitors(True).monitors: + connector = display.get_atom_name(m.name) + print( + f"{connector}, {m.width_in_pixels}x{m.height_in_pixels}, " + f"x={m.x}, y={m.y}" + ) if __name__ == "__main__": pass diff --git a/src/scripts/python/util/randr.py b/src/scripts/python/util/randr.py index 37fe1da..a278ce9 100644 --- a/src/scripts/python/util/randr.py +++ b/src/scripts/python/util/randr.py @@ -23,6 +23,8 @@ from enum import Enum from dataclasses import dataclass from typing import List +from Xlib.ext import randr as rnd + # TODO: Option class which can be applied by get_args method # TODO: Screen-related option class ~ From 62eebad3a3ebf33bc82fdf0f5ae67f6ced60a41e Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 3 Apr 2023 11:10:57 +0330 Subject: [PATCH 38/45] Add prepare monitor This function will return setup of the monitor systems. --- conf/desktop.conf | 3 + setup.sh | 5 ++ src/scripts/python/setupmonitor.py | 98 ++++++++++++++++++++++++++++- src/scripts/python/util/__init__.py | 1 + src/scripts/python/util/common.py | 28 +++++++++ src/scripts/python/util/egalax.py | 4 +- 6 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 conf/desktop.conf create mode 100644 src/scripts/python/util/__init__.py diff --git a/conf/desktop.conf b/conf/desktop.conf new file mode 100644 index 0000000..11d68f1 --- /dev/null +++ b/conf/desktop.conf @@ -0,0 +1,3 @@ +[DEFAULT] +MainDisplay=HDMI-3 +Policy=Mirror diff --git a/setup.sh b/setup.sh index ac18cdc..1904692 100755 --- a/setup.sh +++ b/setup.sh @@ -21,6 +21,11 @@ log '.: Setting up sono-os v0.1.0 :.' sleep 1 draw_progress_bar 5 +log 'Installing dependancies ...' +# TODO +sleep 1 + +draw_progress_bar 15 log 'Installing scripts ...' # TODO sleep 1 diff --git a/src/scripts/python/setupmonitor.py b/src/scripts/python/setupmonitor.py index aa43d9e..e054156 100644 --- a/src/scripts/python/setupmonitor.py +++ b/src/scripts/python/setupmonitor.py @@ -28,6 +28,102 @@ Date: 2023 Mar 04 """ +import Xlib.display +import Xlib.ext.randr +import configparser +from operator import itemgetter +from util import edit_distance +from util.egalax import get_egalax_drm_pure_name + +CONFIG_NAME = "conf/desktop.conf" + + +def read_config(): + """Reads config file of desktop setup + + This function will reeds the config file of desktop. Config file contins + a default monitor port name which will be the main Monitor display. + + NOTE: eGalax devide will be detected automatically from the /dev mountings. + """ + config = configparser.ConfigParser() + read = config.read(CONFIG_NAME) + if not read: + raise FileNotFoundError("Desktop config file not found") + return config + + +def all_connected_monitor(): + """Generates all connected monitors + + as a tuple of (atom name: str, width, height) + """ + display = Xlib.display.Display() + root = display.screen().root + for m in root.xrandr_get_monitors().monitors: + yield ( + display.get_atom_name(m.name), + m.width_in_pixels, + m.height_in_pixels, + ) + + +def get_edid_name(drm_name): + """Change eGalax DRM name to atom name""" + return "#TODO" + + +def prepare_monitors(config): + """Prepare monitor names + + + Rules: + - Use default monitor port as the main monitor if it is connected + - If default monitor is not connected then use a monitor with + minimum edit distance. + + - Use eGalax as touchpanel and if it's not connected return None. + + - other connected monitors will be returned as a list + + each monitor is returen as a + + Returns + tuple: of + - main monitor -> tuple | None if no monitor available other than + touchpanel + - touchpanel -> tuple | None if touchpanel did not exist + - other -> list: this list may be empty if there is no other + monitor connected + """ + main = config["DEFAULT"]["MainDisplay"] + all_monitors = list(all_connected_monitor()) + egalax_drm = get_egalax_drm_pure_name() + egalax_name = get_edid_name(egalax_drm) + egalax_monitor = None + main_monitor = None + for mon in all_monitors: + if egalax_name == mon[0]: + egalax_monitor = mon + if main == mon[0]: + main_monitor = mon + if egalax_monitor: + all_monitors.remove(egalax_monitor) + if not main_monitor: + try: + min_monitor = min( + all_monitors, + key=lambda x: edit_distance(main, x, len(main), len(x)), + ) + main_monitor = min_monitor + except: + main_monitor = None + assert len(all_monitors) == 0 + if main_monitor: + all_monitors.remove(main_monitor) + return main_monitor, egalax_monitor, all_monitors + if __name__ == "__main__": - pass + conf = read_config() + print(prepare_monitors(conf)) diff --git a/src/scripts/python/util/__init__.py b/src/scripts/python/util/__init__.py new file mode 100644 index 0000000..7d9dd68 --- /dev/null +++ b/src/scripts/python/util/__init__.py @@ -0,0 +1 @@ +from .common import edit_distance, max_match diff --git a/src/scripts/python/util/common.py b/src/scripts/python/util/common.py index 6e2bd91..2ee76e4 100644 --- a/src/scripts/python/util/common.py +++ b/src/scripts/python/util/common.py @@ -33,3 +33,31 @@ def max_match(a: str, b: str) -> str: i += 1 else: return a[0:i] + + +def edit_distance(str1, str2, m, n): + # If first string is empty, the only option is to + # insert all characters of second string into first + if m == 0: + return n + + # If second string is empty, the only option is to + # remove all characters of first string + if n == 0: + return m + + # If last characters of two strings are same, nothing + # much to do. Ignore last characters and get count for + # remaining strings. + if str1[m - 1] == str2[n - 1]: + return edit_distance(str1, str2, m - 1, n - 1) + + # If last characters are not same, consider all three + # operations on last character of first string, recursively + # compute minimum cost for all three operations and take + # minimum of three values. + return 1 + min( + edit_distance(str1, str2, m, n - 1), # Insert + edit_distance(str1, str2, m - 1, n), # Remove + edit_distance(str1, str2, m - 1, n - 1), # Replace + ) diff --git a/src/scripts/python/util/egalax.py b/src/scripts/python/util/egalax.py index 1cf43aa..3056fb4 100644 --- a/src/scripts/python/util/egalax.py +++ b/src/scripts/python/util/egalax.py @@ -5,8 +5,8 @@ touchpannel's drm output and its overal status. """ from pathlib import Path -from x import get_edid_dev_path -from common import max_match +from .x import get_edid_dev_path +from .common import max_match VENDOR_ID = "0EEF" DEVICE_ID = "C000" From 7120888afe89a62d3cfc26056ee4b0c9235cda8b Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Mon, 3 Apr 2023 11:19:22 +0330 Subject: [PATCH 39/45] Implement get edid name --- src/scripts/python/setupmonitor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/scripts/python/setupmonitor.py b/src/scripts/python/setupmonitor.py index e054156..0be74ca 100644 --- a/src/scripts/python/setupmonitor.py +++ b/src/scripts/python/setupmonitor.py @@ -68,9 +68,11 @@ def all_connected_monitor(): ) -def get_edid_name(drm_name): +def get_edid_name(drm_name: str): """Change eGalax DRM name to atom name""" - return "#TODO" + card_num, name = drm_name[4:].split("-", maxsplit=1) + first, second = name.rsplit("-", maxsplit=1) + return first + "-" + card_num + "-" + second def prepare_monitors(config): From 6577f79250e6a5f8e52a07da54bd8c9282cb4824 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Thu, 6 Apr 2023 21:00:11 +0330 Subject: [PATCH 40/45] Add suitable enviornment for rule xinput needs DISPLAY and XAUTHORITY variables set to work. --- src/rules/90-hid-mouse.rules | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rules/90-hid-mouse.rules b/src/rules/90-hid-mouse.rules index b392d93..37e0e3e 100644 --- a/src/rules/90-hid-mouse.rules +++ b/src/rules/90-hid-mouse.rules @@ -3,7 +3,9 @@ # insertion, removal. Actually, usb events are triggered when usb and # usb interface are binding but hid is more specific and happen once for each # action. change action is not used because -SUBSYSTEM=="hid", ACTION=="bind", RUN+="/usr/local/bin/changemouse.py" +# XAUTHORUTY and DISPLAY must be set and related to the current session. It may +# vary with different graphic drivers. +SUBSYSTEM=="hid", ENV{DISPLAY}=":0", ENV{XAUTHORITY}="/run/user/1000/gdm/Xauthority", ACTION=="bind", RUN+="/usr/local/bin/changemouse.py" # the following line may not be needed as when we unplug a mouse it would # automatically remove devices from the list and they need no furthur From 25a054bd21f5d16f576adb48aabca1c77db8d70c Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Fri, 7 Apr 2023 16:27:42 +0330 Subject: [PATCH 41/45] Add xrandr policy --- src/scripts/python/setupmonitor.py | 88 ++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 5 deletions(-) diff --git a/src/scripts/python/setupmonitor.py b/src/scripts/python/setupmonitor.py index 0be74ca..501bb5a 100644 --- a/src/scripts/python/setupmonitor.py +++ b/src/scripts/python/setupmonitor.py @@ -31,12 +31,12 @@ Date: 2023 Mar 04 import Xlib.display import Xlib.ext.randr import configparser -from operator import itemgetter from util import edit_distance from util.egalax import get_egalax_drm_pure_name +import subprocess CONFIG_NAME = "conf/desktop.conf" - +XRANDR = "/usr/bin/xrandr" def read_config(): """Reads config file of desktop setup @@ -69,7 +69,11 @@ def all_connected_monitor(): def get_edid_name(drm_name: str): - """Change eGalax DRM name to atom name""" + """Change eGalax DRM name to atom name + + This function is very sensitive to kernel version and might not work + with some kernels. + """ card_num, name = drm_name[4:].split("-", maxsplit=1) first, second = name.rsplit("-", maxsplit=1) return first + "-" + card_num + "-" + second @@ -101,7 +105,7 @@ def prepare_monitors(config): main = config["DEFAULT"]["MainDisplay"] all_monitors = list(all_connected_monitor()) egalax_drm = get_egalax_drm_pure_name() - egalax_name = get_edid_name(egalax_drm) + egalax_name = get_edid_name(egalax_drm) if egalax_drm else None egalax_monitor = None main_monitor = None for mon in all_monitors: @@ -126,6 +130,80 @@ def prepare_monitors(config): return main_monitor, egalax_monitor, all_monitors +def baseline(main, egalax): + """Base of xrandr arguments + + Both main and egalax are monitor tuples mentioned in prepare_monitors""" + if not main and not egalax: + return [], None + elif not main and egalax: + return ["--output", egalax[0], "--primary", + "--mode", f"{egalax[1]}x{egalax[2]}"], None + elif main and not egalax: + return ["--output", main[0], "--primary", + "--mode", f"{main[1]}x{main[2]}"], main[0] + else: + return ["--output", main[0], "--primary", + "--mode", f"{main[1]}x{main[2]}", + "--output", egalax[0], "--right-of", main[0], + "--mode", f"{egalax[1]}x{egalax[1]}"], egalax[0] + + +def mirror_displays(main, egalax, others: list): + base, should_mirror = baseline(main, egalax) + if should_mirror: + for name, width, height in others: + base.extend(["--output", name, "--mode", f"{width}x{height}", + "--same-as", main[0], + "--scale-from", f"{main[1]}x{main[2]}"]) + return base + + +def off_displays(main, egalax, others: list): + base, should_off = baseline(main, egalax) + if should_off: + for name, width, height in others: + base.extend(["--output", name, "--off"]) + return base + + +def stand_alone_displays(main, egalax, others: list): + base, rightmost = baseline(main, egalax) + if rightmost: + for name, width, height in others: + base.extend(["--output", name, "--mode", f"{width}x{height}", + "--right-of", rightmost]) + rightmost = name + return base + + +POLICY = { + 'Off': off_displays, + 'Mirror': mirror_displays, + 'StandAlone': stand_alone_displays +} + + +def config_xrandr(conf, main, egalax, others: list): + """Executes xrandr with policy in the conf + + Policies: + Policies are about monitors other than main and touch panel monitors. + There are three supported policies: + - Off: Disables other monitors (default policy if not in config) + - Mirror: Mirror other displays from main monitor. + - StandAlone: Each monitor is mapped to the right of each other + """ + try: + policy = conf['DEFAULT']['Policy'] + except: + policy = 'Off' + args = POLICY[policy](main, egalax, others) + cmd = [XRANDR] + args + subprocess.run(cmd) + + if __name__ == "__main__": conf = read_config() - print(prepare_monitors(conf)) + main, egalax, others = prepare_monitors(conf) + config_xrandr(conf, main, egalax, others) From 39a481eb565f388b5de4c6865cc3ead9d52da429 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Fri, 7 Apr 2023 17:08:59 +0330 Subject: [PATCH 42/45] Add drm rules for monitor change event --- src/rules/90-drm.rules | 1 + src/scripts/setupmonitor.sh | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 src/rules/90-drm.rules create mode 100755 src/scripts/setupmonitor.sh diff --git a/src/rules/90-drm.rules b/src/rules/90-drm.rules new file mode 100644 index 0000000..cbd4186 --- /dev/null +++ b/src/rules/90-drm.rules @@ -0,0 +1 @@ +SUBSYSTEM=="drm", ENV{MONITOR_LOCK}="/tmp/monitorlock", ENV{SONOLOG}="/tmp/sonolog.log", RUN+="/usr/local/bin/setupmonitor.sh" \ No newline at end of file diff --git a/src/scripts/setupmonitor.sh b/src/scripts/setupmonitor.sh new file mode 100755 index 0000000..40552d7 --- /dev/null +++ b/src/scripts/setupmonitor.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# This script will run when drm change event detected. +# This sctipt should be placed in /usr/local/bin +# SONOLOG file must be set beforehand in the udev rule +# MONITOR_LOCK should be set +( + flock -n 100 || exit 1 + sleep 1 # wait until all changes take place + xrandr --auto + python3 /usr/bin/local/python/setupmonitor.py + echo $(data) - INFO - Setup Monitor Done >> $SONOLOG +) 100> $MONITOR_LOCK \ No newline at end of file From 2e4b471eaca121d1d68c7b29748880708168a0d2 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Fri, 7 Apr 2023 17:27:06 +0330 Subject: [PATCH 43/45] Move conf folder to src/script/python --- {conf => src/scripts/python/conf}/desktop.conf | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {conf => src/scripts/python/conf}/desktop.conf (100%) diff --git a/conf/desktop.conf b/src/scripts/python/conf/desktop.conf similarity index 100% rename from conf/desktop.conf rename to src/scripts/python/conf/desktop.conf From 6fa9e4f10366026a437315414fbdeaa5d360b032 Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Fri, 7 Apr 2023 17:28:17 +0330 Subject: [PATCH 44/45] Change relative config file to absolute --- src/scripts/python/setupmonitor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/scripts/python/setupmonitor.py b/src/scripts/python/setupmonitor.py index 501bb5a..7f71d69 100644 --- a/src/scripts/python/setupmonitor.py +++ b/src/scripts/python/setupmonitor.py @@ -34,8 +34,10 @@ import configparser from util import edit_distance from util.egalax import get_egalax_drm_pure_name import subprocess +from pathlib import Path +import os -CONFIG_NAME = "conf/desktop.conf" +CONFIG_NAME = Path(os.path.dirname(os.path.realpath(__file__))) / "conf/desktop.conf" XRANDR = "/usr/bin/xrandr" def read_config(): From bcc549cbb6b84ad9f0e20ba609d33cef72cf502f Mon Sep 17 00:00:00 2001 From: Ali Hatami Tajik Date: Fri, 7 Apr 2023 23:36:02 +0330 Subject: [PATCH 45/45] Remove dispatches from x.py --- src/scripts/python/util/x.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/scripts/python/util/x.py b/src/scripts/python/util/x.py index 8fd0c6f..aef22aa 100644 --- a/src/scripts/python/util/x.py +++ b/src/scripts/python/util/x.py @@ -1,11 +1,9 @@ import subprocess -from multipledispatch import dispatch from pathlib import Path ENCODING = "utf-8" -@dispatch() def get_list_short(): """Returns string output of the `xinput --list --short` command encoded as UTF-8""" @@ -15,7 +13,6 @@ def get_list_short(): return completed.stdout.decode(ENCODING) -@dispatch(int) def get_list_short_with(id): """Short List of the id