mirror of
https://codeberg.org/privacy1st/rotate-screen
synced 2024-11-26 22:15:02 +01:00
init
This commit is contained in:
commit
d6e11fcdf9
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/.idea/
|
121
LICENSE
Normal file
121
LICENSE
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
Creative Commons Legal Code
|
||||||
|
|
||||||
|
CC0 1.0 Universal
|
||||||
|
|
||||||
|
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||||
|
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||||
|
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||||
|
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||||
|
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||||
|
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||||
|
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||||
|
HEREUNDER.
|
||||||
|
|
||||||
|
Statement of Purpose
|
||||||
|
|
||||||
|
The laws of most jurisdictions throughout the world automatically confer
|
||||||
|
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||||
|
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||||
|
authorship and/or a database (each, a "Work").
|
||||||
|
|
||||||
|
Certain owners wish to permanently relinquish those rights to a Work for
|
||||||
|
the purpose of contributing to a commons of creative, cultural and
|
||||||
|
scientific works ("Commons") that the public can reliably and without fear
|
||||||
|
of later claims of infringement build upon, modify, incorporate in other
|
||||||
|
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||||
|
and for any purposes, including without limitation commercial purposes.
|
||||||
|
These owners may contribute to the Commons to promote the ideal of a free
|
||||||
|
culture and the further production of creative, cultural and scientific
|
||||||
|
works, or to gain reputation or greater distribution for their Work in
|
||||||
|
part through the use and efforts of others.
|
||||||
|
|
||||||
|
For these and/or other purposes and motivations, and without any
|
||||||
|
expectation of additional consideration or compensation, the person
|
||||||
|
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||||
|
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||||
|
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||||
|
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||||
|
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||||
|
|
||||||
|
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||||
|
protected by copyright and related or neighboring rights ("Copyright and
|
||||||
|
Related Rights"). Copyright and Related Rights include, but are not
|
||||||
|
limited to, the following:
|
||||||
|
|
||||||
|
i. the right to reproduce, adapt, distribute, perform, display,
|
||||||
|
communicate, and translate a Work;
|
||||||
|
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||||
|
iii. publicity and privacy rights pertaining to a person's image or
|
||||||
|
likeness depicted in a Work;
|
||||||
|
iv. rights protecting against unfair competition in regards to a Work,
|
||||||
|
subject to the limitations in paragraph 4(a), below;
|
||||||
|
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||||
|
in a Work;
|
||||||
|
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||||
|
European Parliament and of the Council of 11 March 1996 on the legal
|
||||||
|
protection of databases, and under any national implementation
|
||||||
|
thereof, including any amended or successor version of such
|
||||||
|
directive); and
|
||||||
|
vii. other similar, equivalent or corresponding rights throughout the
|
||||||
|
world based on applicable law or treaty, and any national
|
||||||
|
implementations thereof.
|
||||||
|
|
||||||
|
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||||
|
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||||
|
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||||
|
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||||
|
of action, whether now known or unknown (including existing as well as
|
||||||
|
future claims and causes of action), in the Work (i) in all territories
|
||||||
|
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||||
|
treaty (including future time extensions), (iii) in any current or future
|
||||||
|
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||||
|
including without limitation commercial, advertising or promotional
|
||||||
|
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||||
|
member of the public at large and to the detriment of Affirmer's heirs and
|
||||||
|
successors, fully intending that such Waiver shall not be subject to
|
||||||
|
revocation, rescission, cancellation, termination, or any other legal or
|
||||||
|
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||||
|
as contemplated by Affirmer's express Statement of Purpose.
|
||||||
|
|
||||||
|
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||||
|
be judged legally invalid or ineffective under applicable law, then the
|
||||||
|
Waiver shall be preserved to the maximum extent permitted taking into
|
||||||
|
account Affirmer's express Statement of Purpose. In addition, to the
|
||||||
|
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||||
|
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||||
|
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||||
|
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||||
|
maximum duration provided by applicable law or treaty (including future
|
||||||
|
time extensions), (iii) in any current or future medium and for any number
|
||||||
|
of copies, and (iv) for any purpose whatsoever, including without
|
||||||
|
limitation commercial, advertising or promotional purposes (the
|
||||||
|
"License"). The License shall be deemed effective as of the date CC0 was
|
||||||
|
applied by Affirmer to the Work. Should any part of the License for any
|
||||||
|
reason be judged legally invalid or ineffective under applicable law, such
|
||||||
|
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||||
|
of the License, and in such case Affirmer hereby affirms that he or she
|
||||||
|
will not (i) exercise any of his or her remaining Copyright and Related
|
||||||
|
Rights in the Work or (ii) assert any associated claims and causes of
|
||||||
|
action with respect to the Work, in either case contrary to Affirmer's
|
||||||
|
express Statement of Purpose.
|
||||||
|
|
||||||
|
4. Limitations and Disclaimers.
|
||||||
|
|
||||||
|
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||||
|
surrendered, licensed or otherwise affected by this document.
|
||||||
|
b. Affirmer offers the Work as-is and makes no representations or
|
||||||
|
warranties of any kind concerning the Work, express, implied,
|
||||||
|
statutory or otherwise, including without limitation warranties of
|
||||||
|
title, merchantability, fitness for a particular purpose, non
|
||||||
|
infringement, or the absence of latent or other defects, accuracy, or
|
||||||
|
the present or absence of errors, whether or not discoverable, all to
|
||||||
|
the greatest extent permissible under applicable law.
|
||||||
|
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||||
|
that may apply to the Work or any use thereof, including without
|
||||||
|
limitation any person's Copyright and Related Rights in the Work.
|
||||||
|
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||||
|
consents, permissions or other rights required for any use of the
|
||||||
|
Work.
|
||||||
|
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||||
|
party to this document and has no duty or obligation with respect to
|
||||||
|
this CC0 or use of the Work.
|
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# RotateScreen
|
||||||
|
|
||||||
|
## Further ideas
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_sensor_orientation():
|
||||||
|
"""
|
||||||
|
Get rotation from `monitor-sensor`.
|
||||||
|
Example:
|
||||||
|
- stdout: === Has accelerometer (orientation: left-up)
|
||||||
|
- stdout: Accelerometer orientation changed: left-up
|
||||||
|
"""
|
||||||
|
```
|
110
de-p1st-rotate.py
Normal file
110
de-p1st-rotate.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import configparser
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
orientations = ['normal', 'right', 'inverted', 'left']
|
||||||
|
|
||||||
|
|
||||||
|
class Screen(NamedTuple):
|
||||||
|
name: str
|
||||||
|
devices: list[str]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
rotate_clockwise()
|
||||||
|
|
||||||
|
|
||||||
|
def rotate_clockwise():
|
||||||
|
cfg = get_cfg()
|
||||||
|
|
||||||
|
# screens from cfg that are connected
|
||||||
|
screens = [Screen(name=name, devices=list(cfg[name].values()))
|
||||||
|
for name in cfg
|
||||||
|
if name != cfg.default_section
|
||||||
|
and is_connected(name)]
|
||||||
|
|
||||||
|
if len(screens) == 0:
|
||||||
|
raise Exception('None of the configured screens are connected.')
|
||||||
|
|
||||||
|
current_orientation = get_current_orientation(screens[0].name)
|
||||||
|
next_orientation = orientations[(orientations.index(current_orientation) + 1) % len(orientations)]
|
||||||
|
|
||||||
|
for screen in screens:
|
||||||
|
rotate(screen, next_orientation)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cfg() -> configparser.ConfigParser:
|
||||||
|
config: configparser.ConfigParser = configparser.ConfigParser()
|
||||||
|
config.read(get_cfg_path())
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def get_cfg_path() -> Path:
|
||||||
|
global_path = Path('/etc/rotate-screen.cfg')
|
||||||
|
if global_path.exists():
|
||||||
|
return global_path
|
||||||
|
|
||||||
|
local_path = Path('example.cfg')
|
||||||
|
if local_path.exists():
|
||||||
|
return local_path
|
||||||
|
|
||||||
|
raise Exception('No configuration file found.')
|
||||||
|
|
||||||
|
|
||||||
|
def rotate(screen: Screen, orientation):
|
||||||
|
execute(['xrandr', '--output', screen.name, '--rotate', orientation])
|
||||||
|
for device in screen.devices:
|
||||||
|
execute(['xrandr', '--map-to-output', device, screen.name])
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_orientation(screen: str):
|
||||||
|
"""
|
||||||
|
@precond: is_connected(screen) = True
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- stdout includes line: eDP connected primary 2880x1620+0+0 (0x55) normal (normal left inverted right x axis y axis) 344mm x 194mm
|
||||||
|
- screen: eDP
|
||||||
|
- returns: normal
|
||||||
|
"""
|
||||||
|
stdout = execute(['xrandr', '--query', '--verbose'])
|
||||||
|
pattern = re.compile(rf'^({re.escape(screen)}\s.*)$', flags=re.MULTILINE)
|
||||||
|
match = pattern.search(stdout)
|
||||||
|
if match is None: raise Exception(f'Did not find screen {screen} in stdout:\n{stdout}')
|
||||||
|
line = match.group(1)
|
||||||
|
return line.rsplit()[5]
|
||||||
|
|
||||||
|
|
||||||
|
def is_connected(screen: str):
|
||||||
|
"""
|
||||||
|
Example:
|
||||||
|
- stdout includes line: eDP connected primary 2880x1620+0+0 (normal left inverted right x axis y axis) 344mm x 194mm
|
||||||
|
- returns: True
|
||||||
|
"""
|
||||||
|
stdout = execute(['xrandr'])
|
||||||
|
pattern = re.compile(rf'^({re.escape(screen)}\sconnected\s.*)$', flags=re.MULTILINE)
|
||||||
|
match = pattern.search(stdout)
|
||||||
|
return match is not None
|
||||||
|
|
||||||
|
|
||||||
|
def execute(command: list[str]) -> str:
|
||||||
|
"""
|
||||||
|
:return: stdout of command execution
|
||||||
|
"""
|
||||||
|
completed: subprocess.CompletedProcess = subprocess.run(
|
||||||
|
command,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if completed.returncode != 0:
|
||||||
|
raise Exception(f'Exit Code: {completed.returncode}\n'
|
||||||
|
f'Stdout:\n{completed.stdout}\n'
|
||||||
|
f'Stderr:\n{completed.stderr}')
|
||||||
|
return completed.stdout
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
13
example.cfg
Normal file
13
example.cfg
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[eDP]
|
||||||
|
|
||||||
|
[HDMI-A-0]
|
||||||
|
|
||||||
|
[eDP-1]
|
||||||
|
|
||||||
|
1 = pointer:ELAN9038:00 04F3:2A1C
|
||||||
|
2 = pointer:ELAN9038:00 04F3:2A1C Stylus Pen (0)
|
||||||
|
3 = pointer:ELAN9038:00 04F3:2A1C Stylus Eraser (0)
|
||||||
|
|
||||||
|
4 = pointer:ELAN9038:00 04F3:2A1C touch
|
||||||
|
5 = pointer:ELAN9038:00 04F3:2A1C stylus
|
||||||
|
6 = pointer:ELAN9038:00 04F3:2A1C eraser
|
Loading…
Reference in New Issue
Block a user