Add input support to the sono-os #5
Merged
hatam
merged 49 commits from input
into master
2 years ago
17 changed files with 1710 additions and 0 deletions
@ -0,0 +1,49 @@ |
|||||
|
#!/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 |
||||
|
|
||||
|
enable_trapping |
||||
|
draw_progress_bar 0 |
||||
|
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 |
||||
|
|
||||
|
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 |
||||
|
destroy_scroll_area |
@ -0,0 +1 @@ |
|||||
|
SUBSYSTEM=="drm", ENV{MONITOR_LOCK}="/tmp/monitorlock", ENV{SONOLOG}="/tmp/sonolog.log", RUN+="/usr/local/bin/setupmonitor.sh" |
@ -0,0 +1,13 @@ |
|||||
|
# 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 |
||||
|
# 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 |
||||
|
# configuration |
||||
|
# SUBSYSTEM=="hid", ACTION=="unbind", RUN+="/usr/local/bin/changemouse.py" |
@ -0,0 +1,46 @@ |
|||||
|
#!/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: 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 |
||||
|
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 util.pointer as putil |
||||
|
import util.x as xutil |
||||
|
|
||||
|
if __name__ == "__main__": |
||||
|
"""Configure Pointers |
||||
|
|
||||
|
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) |
@ -0,0 +1,3 @@ |
|||||
|
[DEFAULT] |
||||
|
MainDisplay=HDMI-3 |
||||
|
Policy=Mirror |
@ -0,0 +1,211 @@ |
|||||
|
"""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: |
||||
|
|
||||
|
""" |
||||
|
|
||||
|
import Xlib.display |
||||
|
import Xlib.ext.randr |
||||
|
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 = Path(os.path.dirname(os.path.realpath(__file__))) / "conf/desktop.conf" |
||||
|
XRANDR = "/usr/bin/xrandr" |
||||
|
|
||||
|
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: str): |
||||
|
"""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 |
||||
|
|
||||
|
|
||||
|
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) if egalax_drm else None |
||||
|
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 |
||||
|
|
||||
|
|
||||
|
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() |
||||
|
main, egalax, others = prepare_monitors(conf) |
||||
|
config_xrandr(conf, main, egalax, others) |
@ -0,0 +1,64 @@ |
|||||
|
from abc import ABC, abstractmethod |
||||
|
from pyudev import Context, Monitor, MonitorObserver, Device |
||||
|
|
||||
|
|
||||
|
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: 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) |
||||
|
|
||||
|
|
||||
|
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) |
@ -0,0 +1 @@ |
|||||
|
from .common import edit_distance, max_match |
@ -0,0 +1,63 @@ |
|||||
|
"""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 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 |
||||
|
|
||||
|
for c in a: |
||||
|
if b[i] == c: |
||||
|
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 |
||||
|
) |
@ -0,0 +1,80 @@ |
|||||
|
"""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 |
||||
|
from .x import get_edid_dev_path |
||||
|
from .common import max_match |
||||
|
|
||||
|
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) |
||||
|
|
||||
|
|
||||
|
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 |
||||
|
|
||||
|
|
||||
|
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 |
@ -0,0 +1,198 @@ |
|||||
|
import util.x as xutil |
||||
|
|
||||
|
# Pointer states |
||||
|
SLAVE, MASTER, FLOATING = range(3) |
||||
|
|
||||
|
|
||||
|
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, state) -> 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.state = state |
||||
|
|
||||
|
|
||||
|
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, state) -> None: |
||||
|
super().__init__(name, id, state) |
||||
|
|
||||
|
def __repr__(self) -> str: |
||||
|
return f"<Pointer: {self.name}-{self.id}-{self.state}>" |
||||
|
|
||||
|
@property |
||||
|
def slave(self): |
||||
|
return self.state == SLAVE |
||||
|
|
||||
|
@property |
||||
|
def master(self): |
||||
|
return self.state == MASTER |
||||
|
|
||||
|
def floating(self): |
||||
|
return self.state == FLOATING |
||||
|
|
||||
|
|
||||
|
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 = xutil.get_list_short_with(id) |
||||
|
name, props = desc.rsplit("id=", 1) |
||||
|
if "pointer" in props: |
||||
|
state = FLOATING |
||||
|
if "master" in props: |
||||
|
state = MASTER |
||||
|
elif "slave" in props: |
||||
|
state = SLAVE |
||||
|
return Pointer(name.strip(), props.split(maxsplit=1)[0], state) |
||||
|
else: |
||||
|
raise TypeError(f"id[{id}] is not a pointer id") |
||||
|
|
||||
|
|
||||
|
def get_ids_iter(): |
||||
|
"""xinput id generator |
||||
|
|
||||
|
Yields: |
||||
|
int: id of xinput devices |
||||
|
""" |
||||
|
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: |
||||
|
yield get_short_pointer(id) |
||||
|
except TypeError as e: |
||||
|
# ignore if the id is not pointer |
||||
|
pass |
||||
|
except e: |
||||
|
# TODO: logging |
||||
|
pass |
||||
|
else: |
||||
|
pass # TODO |
||||
|
|
||||
|
|
||||
|
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 = [] |
||||
|
for pointer in get_pointer_iter(is_short): |
||||
|
pointers.append(pointer) |
||||
|
return pointers |
||||
|
|
||||
|
|
||||
|
def create_touch_master(): |
||||
|
"""_summary_ |
||||
|
|
||||
|
Raises: |
||||
|
SystemError: If creation of touch pointer failed |
||||
|
|
||||
|
Returns: |
||||
|
Pointer: pointer object corresponding to `touch` master |
||||
|
""" |
||||
|
touch = None |
||||
|
xutil.create_master("touch") |
||||
|
id = xutil.get_xi_id_by_name("touch pointer") |
||||
|
if id: |
||||
|
try: |
||||
|
touch = get_short_pointer(id) |
||||
|
except: |
||||
|
raise SystemError( |
||||
|
"touch pointer is not available. cannot create touch pointer" |
||||
|
) |
||||
|
else: |
||||
|
raise SystemError( |
||||
|
"touch pointer is not available. cannot create touch pointer" |
||||
|
) |
||||
|
|
||||
|
# TODO configure cursor bitmap |
||||
|
return touch |
||||
|
|
||||
|
|
||||
|
def get_pointers_categorized(): |
||||
|
"""Categorized Pointers |
||||
|
|
||||
|
Categories: |
||||
|
1. VCore: Pointer |
||||
|
2. Touch Master: Pointer |
||||
|
3. eGalax: Pointer | None |
||||
|
4. Other non-masters: List[Pinter] |
||||
|
|
||||
|
Raises: |
||||
|
SystemError: If creation of touch pointer failed |
||||
|
|
||||
|
Returns: |
||||
|
|
||||
|
""" |
||||
|
v_core = None |
||||
|
touch = None |
||||
|
e_galax = None |
||||
|
pointers = [] |
||||
|
# filter pointers |
||||
|
for pointer in get_pointer_iter(): |
||||
|
if pointer.name == "Virtual core pointer": |
||||
|
v_core = pointer |
||||
|
elif "eGalax" in pointer.name: |
||||
|
e_galax = pointer |
||||
|
elif pointer.name == "touch pointer": |
||||
|
touch = pointer |
||||
|
elif not pointer.master: |
||||
|
pointers.append(pointer) |
||||
|
|
||||
|
if not touch: |
||||
|
touch = create_touch_master() |
||||
|
|
||||
|
return v_core, touch, e_galax, pointers |
@ -0,0 +1,113 @@ |
|||||
|
"""RandR |
||||
|
|
||||
|
Author: Ali Hatami Tajik [hatam](mailto:a.hatam008@gmail.com) |
||||
|
Creation 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 |
||||
|
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 ~ |
||||
|
# TODO: abs position |
||||
|
|
||||
|
|
||||
|
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 |
||||
|
|
||||
|
|
||||
|
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: |
||||
|
"""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 |
||||
|
""" |
@ -0,0 +1,106 @@ |
|||||
|
import subprocess |
||||
|
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() |
||||
|
|
||||
|
|
||||
|
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) |
||||
|
|
||||
|
|
||||
|
def get_list_short_with(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 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(): |
||||
|
"""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())) |
||||
|
|
||||
|
|
||||
|
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 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 None |
||||
|
else: |
||||
|
return int(completed.stdout.decode(ENCODING)) |
||||
|
|
||||
|
|
||||
|
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") |
@ -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 |
@ -0,0 +1,195 @@ |
|||||
|
#!/bin/bash |
||||
|
# https://github.com/pollev/bash_progress_bar - See license at end of file |
||||
|
|
||||
|
# 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. |
@ -0,0 +1,400 @@ |
|||||
|
#!/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 |
||||
|
} |
||||
|
|
||||
|
function log () { |
||||
|
if [[ $_V -eq 1 ]]; then |
||||
|
echo "$@" |
||||
|
fi |
||||
|
} |
||||
|
|
||||
|
# vim: syntax=sh cc=80 tw=79 ts=4 sw=4 sts=4 et sr |
Loading…
Reference in new issue