sunshine-res 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sunshine_res-0.0.1/LICENSE +19 -0
- sunshine_res-0.0.1/PKG-INFO +101 -0
- sunshine_res-0.0.1/README.md +85 -0
- sunshine_res-0.0.1/pyproject.toml +29 -0
- sunshine_res-0.0.1/sunshine_res/__init__.py +84 -0
- sunshine_res-0.0.1/sunshine_res/__main__.py +4 -0
- sunshine_res-0.0.1/sunshine_res/cosmic.py +71 -0
- sunshine_res-0.0.1/sunshine_res/kde.py +102 -0
- sunshine_res-0.0.1/sunshine_res/types.py +123 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
MIT License Copyright (c) 2025 iamthefij
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is furnished
|
|
8
|
+
to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice (including the next
|
|
11
|
+
paragraph) shall be included in all copies or substantial portions of the
|
|
12
|
+
Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
16
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
|
17
|
+
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
18
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
|
19
|
+
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sunshine-res
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Sunshine script to match Linux desktop resolution to client
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: iamthefij
|
|
8
|
+
Requires-Python: >=3.12,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# sunshine-res
|
|
17
|
+
|
|
18
|
+
**sunshine-res** is a small command‑line utility that automatically adjusts your display resolution and HDR settings to match a target resolution that your game or application expects. It works on modern Linux desktop environments that expose a standard way to query and set display modes.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
The project is distributed as a Poetry package. If you already have Poetry installed:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
poetry install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Alternatively, you can install it system‑wide (or inside a virtualenv) with:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The tool installs a console script called `sunshine-res` that can be invoked from anywhere.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
The utility accepts three commands:
|
|
39
|
+
|
|
40
|
+
| Command | Description |
|
|
41
|
+
|---------|-------------|
|
|
42
|
+
| `do` | Set the monitor to the target resolution (as specified by the environment variables below). |
|
|
43
|
+
| `undo` | Revert the monitor to the original mode that was active before the last `do`. |
|
|
44
|
+
| `auto` | Toggle between the two states. If a previous `do` was performed, `undo` will be run; otherwise `do` will be executed. This is the default if no command is given. |
|
|
45
|
+
|
|
46
|
+
### Configuring Sunshine
|
|
47
|
+
|
|
48
|
+
1. Open Sunshine and go to the Configuration tab (probably https://localhost:47990/config)
|
|
49
|
+
2. In the Command Preparations section, click "+ Add" to add a new command
|
|
50
|
+
3. Set the do and undo commands to `sunshine-res do` and `sunshine-res undo` respectively
|
|
51
|
+
4. Save
|
|
52
|
+
|
|
53
|
+
### Example
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Set resolution to 1920×1080 @ 60 Hz, HDR disabled
|
|
57
|
+
SUNSHINE_CLIENT_WIDTH=1920 SUNSHINE_CLIENT_HEIGHT=1080 SUNSHINE_CLIENT_FPS=60 SUNSHINE_CLIENT_HDR=false sunshine-res do
|
|
58
|
+
|
|
59
|
+
# Revert to original resolution
|
|
60
|
+
sunshine-res undo
|
|
61
|
+
|
|
62
|
+
# Toggle
|
|
63
|
+
sunshine-res
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Environment variables are optional; defaults are:
|
|
67
|
+
|
|
68
|
+
- `SUNSHINE_CLIENT_WIDTH` – 1920
|
|
69
|
+
- `SUNSHINE_CLIENT_HEIGHT` – 1080
|
|
70
|
+
- `SUNSHINE_CLIENT_FPS` – 60
|
|
71
|
+
- `SUNSHINE_CLIENT_HDR` – false
|
|
72
|
+
|
|
73
|
+
## Supported Desktop Environments
|
|
74
|
+
|
|
75
|
+
| Desktop | Implementation |
|
|
76
|
+
|---------|-----------------|
|
|
77
|
+
| KDE | Uses `kscreen-doctor` to query and set monitor modes. |
|
|
78
|
+
| COSMIC | Uses `cosmic-randr` to query and set monitor modes. |
|
|
79
|
+
|
|
80
|
+
The tool automatically detects the current desktop by inspecting the `XDG_CURRENT_DESKTOP`, `XDG_SESSION_DESKTOP`, or `SESSION_DESKTOP` environment variables. If no supported desktop is detected, the command will exit with an error.
|
|
81
|
+
|
|
82
|
+
## How It Works
|
|
83
|
+
|
|
84
|
+
1. **Detect Desktop** – The first step is to determine whether you are running KDE or Cosmic.
|
|
85
|
+
2. **Query Current Mode** – The relevant command (`kscreen-doctor` or `cosmic-randr`) returns a JSON/KDL description of the current monitor configuration.
|
|
86
|
+
3. **Find Matching Mode** – The tool searches the available modes for one that matches the target resolution and has the closest refresh rate that is not below the requested `SUNSHINE_CLIENT_FPS`.
|
|
87
|
+
4. **Apply Mode** – The chosen mode is applied with the same HDR setting you requested.
|
|
88
|
+
5. **Persist State** – The original monitor state is written to `~/.config/sunshine/last_mode.json` so that `undo` can restore it later.
|
|
89
|
+
|
|
90
|
+
## Common Errors & Troubleshooting
|
|
91
|
+
|
|
92
|
+
| Error | What it means | How to fix |
|
|
93
|
+
|-------|----------------|------------|
|
|
94
|
+
| `ERROR: Could not determine current desktop` | No desktop environment variable was found. | Make sure you are running the command from an active session. On some shells you may need to export `XDG_CURRENT_DESKTOP=KDE` or `COSMIC` manually. |
|
|
95
|
+
| `Could not find resolution manager for desktop <name>` | The desktop variable contains an unsupported value. | Check the spelling of the desktop name. Supported values are `KDE` and `COSMIC`. |
|
|
96
|
+
| `Did not find mode matching <width>x<height> at <output>` | The monitor does not advertise the requested resolution. | Verify that the resolution is supported by your monitor or try a different resolution. |
|
|
97
|
+
| `Could not identify current mode` | The underlying command returned an unexpected format. | Ensure `cosmic-randr` or `kscreen-doctor` is installed and working. Run the command manually (`cosmic-randr list --kdl` or `kscreen-doctor --json`) to confirm. |
|
|
98
|
+
| `Permission denied` | The command was unable to write to `~/.config/sunshine/last_mode.json`. | Verify that you have write permissions to `~/.config/sunshine`. |
|
|
99
|
+
|
|
100
|
+
If you encounter an unhandled exception, try running the command with the `-v` flag (if implemented) or inspect the stack trace. The project is open source, so feel free to file an issue.
|
|
101
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# sunshine-res
|
|
2
|
+
|
|
3
|
+
**sunshine-res** is a small command‑line utility that automatically adjusts your display resolution and HDR settings to match a target resolution that your game or application expects. It works on modern Linux desktop environments that expose a standard way to query and set display modes.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
The project is distributed as a Poetry package. If you already have Poetry installed:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
poetry install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Alternatively, you can install it system‑wide (or inside a virtualenv) with:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The tool installs a console script called `sunshine-res` that can be invoked from anywhere.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
The utility accepts three commands:
|
|
24
|
+
|
|
25
|
+
| Command | Description |
|
|
26
|
+
|---------|-------------|
|
|
27
|
+
| `do` | Set the monitor to the target resolution (as specified by the environment variables below). |
|
|
28
|
+
| `undo` | Revert the monitor to the original mode that was active before the last `do`. |
|
|
29
|
+
| `auto` | Toggle between the two states. If a previous `do` was performed, `undo` will be run; otherwise `do` will be executed. This is the default if no command is given. |
|
|
30
|
+
|
|
31
|
+
### Configuring Sunshine
|
|
32
|
+
|
|
33
|
+
1. Open Sunshine and go to the Configuration tab (probably https://localhost:47990/config)
|
|
34
|
+
2. In the Command Preparations section, click "+ Add" to add a new command
|
|
35
|
+
3. Set the do and undo commands to `sunshine-res do` and `sunshine-res undo` respectively
|
|
36
|
+
4. Save
|
|
37
|
+
|
|
38
|
+
### Example
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Set resolution to 1920×1080 @ 60 Hz, HDR disabled
|
|
42
|
+
SUNSHINE_CLIENT_WIDTH=1920 SUNSHINE_CLIENT_HEIGHT=1080 SUNSHINE_CLIENT_FPS=60 SUNSHINE_CLIENT_HDR=false sunshine-res do
|
|
43
|
+
|
|
44
|
+
# Revert to original resolution
|
|
45
|
+
sunshine-res undo
|
|
46
|
+
|
|
47
|
+
# Toggle
|
|
48
|
+
sunshine-res
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Environment variables are optional; defaults are:
|
|
52
|
+
|
|
53
|
+
- `SUNSHINE_CLIENT_WIDTH` – 1920
|
|
54
|
+
- `SUNSHINE_CLIENT_HEIGHT` – 1080
|
|
55
|
+
- `SUNSHINE_CLIENT_FPS` – 60
|
|
56
|
+
- `SUNSHINE_CLIENT_HDR` – false
|
|
57
|
+
|
|
58
|
+
## Supported Desktop Environments
|
|
59
|
+
|
|
60
|
+
| Desktop | Implementation |
|
|
61
|
+
|---------|-----------------|
|
|
62
|
+
| KDE | Uses `kscreen-doctor` to query and set monitor modes. |
|
|
63
|
+
| COSMIC | Uses `cosmic-randr` to query and set monitor modes. |
|
|
64
|
+
|
|
65
|
+
The tool automatically detects the current desktop by inspecting the `XDG_CURRENT_DESKTOP`, `XDG_SESSION_DESKTOP`, or `SESSION_DESKTOP` environment variables. If no supported desktop is detected, the command will exit with an error.
|
|
66
|
+
|
|
67
|
+
## How It Works
|
|
68
|
+
|
|
69
|
+
1. **Detect Desktop** – The first step is to determine whether you are running KDE or Cosmic.
|
|
70
|
+
2. **Query Current Mode** – The relevant command (`kscreen-doctor` or `cosmic-randr`) returns a JSON/KDL description of the current monitor configuration.
|
|
71
|
+
3. **Find Matching Mode** – The tool searches the available modes for one that matches the target resolution and has the closest refresh rate that is not below the requested `SUNSHINE_CLIENT_FPS`.
|
|
72
|
+
4. **Apply Mode** – The chosen mode is applied with the same HDR setting you requested.
|
|
73
|
+
5. **Persist State** – The original monitor state is written to `~/.config/sunshine/last_mode.json` so that `undo` can restore it later.
|
|
74
|
+
|
|
75
|
+
## Common Errors & Troubleshooting
|
|
76
|
+
|
|
77
|
+
| Error | What it means | How to fix |
|
|
78
|
+
|-------|----------------|------------|
|
|
79
|
+
| `ERROR: Could not determine current desktop` | No desktop environment variable was found. | Make sure you are running the command from an active session. On some shells you may need to export `XDG_CURRENT_DESKTOP=KDE` or `COSMIC` manually. |
|
|
80
|
+
| `Could not find resolution manager for desktop <name>` | The desktop variable contains an unsupported value. | Check the spelling of the desktop name. Supported values are `KDE` and `COSMIC`. |
|
|
81
|
+
| `Did not find mode matching <width>x<height> at <output>` | The monitor does not advertise the requested resolution. | Verify that the resolution is supported by your monitor or try a different resolution. |
|
|
82
|
+
| `Could not identify current mode` | The underlying command returned an unexpected format. | Ensure `cosmic-randr` or `kscreen-doctor` is installed and working. Run the command manually (`cosmic-randr list --kdl` or `kscreen-doctor --json`) to confirm. |
|
|
83
|
+
| `Permission denied` | The command was unable to write to `~/.config/sunshine/last_mode.json`. | Verify that you have write permissions to `~/.config/sunshine`. |
|
|
84
|
+
|
|
85
|
+
If you encounter an unhandled exception, try running the command with the `-v` flag (if implemented) or inspect the stack trace. The project is open source, so feel free to file an issue.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "sunshine-res"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Sunshine script to match Linux desktop resolution to client"
|
|
9
|
+
authors = ["iamthefij"]
|
|
10
|
+
license = "MIT"
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
|
|
13
|
+
[tool.poetry.scripts]
|
|
14
|
+
sunshine-res = "sunshine_res:main"
|
|
15
|
+
|
|
16
|
+
[tool.poetry.dependencies]
|
|
17
|
+
python = "^3.12"
|
|
18
|
+
|
|
19
|
+
[tool.poetry.group.dev.dependencies]
|
|
20
|
+
black = "^24.4.2"
|
|
21
|
+
isort = "^5.13.2"
|
|
22
|
+
mypy = "^1.10.0"
|
|
23
|
+
coverage = "^7.12.0"
|
|
24
|
+
pre-commit = "^3.7.1"
|
|
25
|
+
pytest = "^9.0.1"
|
|
26
|
+
|
|
27
|
+
[tool.isort]
|
|
28
|
+
force_single_line = true
|
|
29
|
+
profile = "black"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resolution Management Module
|
|
3
|
+
|
|
4
|
+
This module provides a system to dynamically adjust display resolution and HDR settings
|
|
5
|
+
based on the user's desktop environment (KDE or Cosmic). It supports command-line
|
|
6
|
+
operations to 'do' (apply a resolution), 'undo' (revert to original), or 'auto' (toggle
|
|
7
|
+
between modes). The module uses platform-specific tools (kscreen-doctor for KDE,
|
|
8
|
+
cosmic-randr for Cosmic) to interface with the display server.
|
|
9
|
+
|
|
10
|
+
Key Features:
|
|
11
|
+
- Auto-detects desktop environment (KDE/Cosmic)
|
|
12
|
+
- Manages resolution and HDR settings
|
|
13
|
+
- Persistent state tracking for 'undo' operations
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
sunshine-res [do/undo/auto]
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
from sunshine_res.cosmic import CosmicRandr
|
|
23
|
+
from sunshine_res.kde import KscreenDoctor
|
|
24
|
+
from sunshine_res.types import ResolutionManager
|
|
25
|
+
|
|
26
|
+
SUNSHINE_CLIENT_WIDTH = int(os.getenv("SUNSHINE_CLIENT_WIDTH", 1920))
|
|
27
|
+
SUNSHINE_CLIENT_HEIGHT = int(os.getenv("SUNSHINE_CLIENT_HEIGHT", 1080))
|
|
28
|
+
SUNSHINE_CLIENT_FPS = int(os.getenv("SUNSHINE_CLIENT_FPS", 60))
|
|
29
|
+
SUNSHINE_CLIENT_HDR = bool(os.getenv("SUNSHINE_CLIENT_HDR")) == True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
DESKTOP_TO_CLASS: dict[str, type[ResolutionManager]] = {
|
|
33
|
+
"KDE": KscreenDoctor,
|
|
34
|
+
"COSMIC": CosmicRandr,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> None:
|
|
39
|
+
"""Entry point for sunshine-res command line tool."""
|
|
40
|
+
# Get the currently listed desktop
|
|
41
|
+
current_desktop = os.getenv(
|
|
42
|
+
"XDG_CURRENT_DESKTOP",
|
|
43
|
+
os.getenv("XDG_SESSION_DESKTOP", os.getenv("SESSION_DESKTOP")),
|
|
44
|
+
)
|
|
45
|
+
if not current_desktop:
|
|
46
|
+
print("ERROR: Could not determine current desktop")
|
|
47
|
+
exit(1)
|
|
48
|
+
|
|
49
|
+
# Find a manager class that matches
|
|
50
|
+
manager: ResolutionManager
|
|
51
|
+
for desktop in current_desktop.split(":"):
|
|
52
|
+
if mc := DESKTOP_TO_CLASS.get(desktop):
|
|
53
|
+
manager = mc(
|
|
54
|
+
client_width=SUNSHINE_CLIENT_WIDTH,
|
|
55
|
+
client_height=SUNSHINE_CLIENT_HEIGHT,
|
|
56
|
+
client_fps=SUNSHINE_CLIENT_FPS,
|
|
57
|
+
client_hdr=SUNSHINE_CLIENT_HDR,
|
|
58
|
+
)
|
|
59
|
+
break
|
|
60
|
+
else:
|
|
61
|
+
print(f"Could not find resolution manager for desktop {current_desktop}")
|
|
62
|
+
exit(1)
|
|
63
|
+
|
|
64
|
+
# Read the command from args
|
|
65
|
+
command = ""
|
|
66
|
+
if len(sys.argv) < 2:
|
|
67
|
+
command = "auto"
|
|
68
|
+
else:
|
|
69
|
+
command = sys.argv[1]
|
|
70
|
+
|
|
71
|
+
# Execute command in given manager
|
|
72
|
+
if command == "auto":
|
|
73
|
+
manager.toggle()
|
|
74
|
+
elif command == "do":
|
|
75
|
+
manager.do()
|
|
76
|
+
elif command == "undo":
|
|
77
|
+
manager.undo()
|
|
78
|
+
else:
|
|
79
|
+
print(f"Unknown command {command}")
|
|
80
|
+
exit(1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from subprocess import check_call
|
|
3
|
+
from subprocess import check_output
|
|
4
|
+
from typing import override
|
|
5
|
+
|
|
6
|
+
from sunshine_res.types import MonitorInfo
|
|
7
|
+
from sunshine_res.types import MonitorMode
|
|
8
|
+
from sunshine_res.types import ResolutionManager
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CosmicRandr(ResolutionManager):
|
|
12
|
+
|
|
13
|
+
def parse_kdl(self, kdl_str: str) -> MonitorInfo:
|
|
14
|
+
output_name = ""
|
|
15
|
+
modes: list[MonitorMode] = []
|
|
16
|
+
current_mode: MonitorMode | None = None
|
|
17
|
+
|
|
18
|
+
for line in kdl_str.split("\n"):
|
|
19
|
+
line = line.strip()
|
|
20
|
+
|
|
21
|
+
if not line:
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
if output := re.match(r'output\s+"(.+)"', line):
|
|
25
|
+
if output_name and output_name != output.group(1):
|
|
26
|
+
raise ValueError(
|
|
27
|
+
"Detected multiple output names. Check cosmic-randr."
|
|
28
|
+
)
|
|
29
|
+
output_name = output.group(1)
|
|
30
|
+
elif mode := re.match(r"mode\s+(\d+)\s+(\d+)\s+(\d+)", line):
|
|
31
|
+
this_mode = MonitorMode(
|
|
32
|
+
id=None,
|
|
33
|
+
width=int(mode.group(1)),
|
|
34
|
+
height=int(mode.group(2)),
|
|
35
|
+
fps=int(mode.group(3)) / 1000,
|
|
36
|
+
)
|
|
37
|
+
modes.append(this_mode)
|
|
38
|
+
if "current=#true" in line:
|
|
39
|
+
current_mode = this_mode
|
|
40
|
+
|
|
41
|
+
if not current_mode:
|
|
42
|
+
raise ValueError("Could not identify current mode. Check cosmic-randr.")
|
|
43
|
+
|
|
44
|
+
return MonitorInfo(
|
|
45
|
+
output_name=output_name,
|
|
46
|
+
hdr=False,
|
|
47
|
+
modes=modes,
|
|
48
|
+
current_mode=current_mode,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
def query_monitor_info(self) -> MonitorInfo:
|
|
53
|
+
out = check_output(["cosmic-randr", "list", "--kdl"])
|
|
54
|
+
cosmic_info = self.parse_kdl(out.decode())
|
|
55
|
+
return cosmic_info
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
def apply_mode(
|
|
59
|
+
self, output_name: str, mode: MonitorMode, hdr: bool = False
|
|
60
|
+
) -> None:
|
|
61
|
+
_ = check_call(
|
|
62
|
+
[
|
|
63
|
+
"cosmic-randr",
|
|
64
|
+
"mode",
|
|
65
|
+
output_name,
|
|
66
|
+
str(mode["width"]),
|
|
67
|
+
str(mode["height"]),
|
|
68
|
+
"--refresh",
|
|
69
|
+
str(mode["fps"]),
|
|
70
|
+
]
|
|
71
|
+
)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from subprocess import check_call
|
|
3
|
+
from subprocess import check_output
|
|
4
|
+
from typing import TypedDict
|
|
5
|
+
from typing import cast
|
|
6
|
+
from typing import override
|
|
7
|
+
|
|
8
|
+
from sunshine_res.types import MonitorInfo
|
|
9
|
+
from sunshine_res.types import MonitorMode
|
|
10
|
+
from sunshine_res.types import ResolutionManager
|
|
11
|
+
from sunshine_res.types import ScreenSize
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class KScreenMode(TypedDict):
|
|
15
|
+
"""Screen mode from kscreen-doctor."""
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
name: str
|
|
19
|
+
refreshRate: float
|
|
20
|
+
size: ScreenSize
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class KScreenOutput(TypedDict):
|
|
24
|
+
"""Output device info from kscreen-doctor."""
|
|
25
|
+
|
|
26
|
+
connected: bool
|
|
27
|
+
currentModeId: str
|
|
28
|
+
enabled: bool
|
|
29
|
+
hdr: bool
|
|
30
|
+
id: int
|
|
31
|
+
modes: list[KScreenMode]
|
|
32
|
+
name: str
|
|
33
|
+
size: ScreenSize
|
|
34
|
+
sizeMM: ScreenSize
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class KScreenInfo(TypedDict):
|
|
38
|
+
"""Screen information from kscreen-doctor."""
|
|
39
|
+
|
|
40
|
+
currentSize: ScreenSize
|
|
41
|
+
id: int
|
|
42
|
+
maxActiveOutputsCount: int
|
|
43
|
+
maxSize: ScreenSize
|
|
44
|
+
minSize: ScreenSize
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class KScreenResult(TypedDict):
|
|
48
|
+
"""Result returned from kscreen-doctor."""
|
|
49
|
+
|
|
50
|
+
outputs: list[KScreenOutput]
|
|
51
|
+
screen: KScreenInfo
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class KscreenDoctor(ResolutionManager):
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
def query_monitor_info(self) -> MonitorInfo:
|
|
58
|
+
out = check_output(["kscreen-doctor", "--json"])
|
|
59
|
+
screen_info = cast(KScreenResult, json.loads(out))
|
|
60
|
+
|
|
61
|
+
screen_id = screen_info["screen"]["id"]
|
|
62
|
+
output = screen_info["outputs"][screen_id]
|
|
63
|
+
|
|
64
|
+
# Get current mode info for restoring
|
|
65
|
+
modes: list[MonitorMode] = []
|
|
66
|
+
current_mode: MonitorMode | None = None
|
|
67
|
+
current_mode_id = output["currentModeId"]
|
|
68
|
+
for mode in output["modes"]:
|
|
69
|
+
new_mode = MonitorMode(
|
|
70
|
+
id=mode["name"],
|
|
71
|
+
width=mode["size"]["width"],
|
|
72
|
+
height=mode["size"]["height"],
|
|
73
|
+
fps=mode["refreshRate"],
|
|
74
|
+
)
|
|
75
|
+
if mode["id"] == current_mode_id:
|
|
76
|
+
current_mode = new_mode
|
|
77
|
+
modes.append(new_mode)
|
|
78
|
+
|
|
79
|
+
if not current_mode:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"Could not determine the current monitor mode. Check kscreen-doctor."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return MonitorInfo(
|
|
85
|
+
output_name=output["name"],
|
|
86
|
+
current_mode=current_mode,
|
|
87
|
+
modes=modes,
|
|
88
|
+
hdr=output["hdr"],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@override
|
|
92
|
+
def apply_mode(
|
|
93
|
+
self, output_name: str, mode: MonitorMode, hdr: bool = False
|
|
94
|
+
) -> None:
|
|
95
|
+
# Set the new display mode
|
|
96
|
+
_ = check_call(
|
|
97
|
+
[
|
|
98
|
+
"kscreen-doctor",
|
|
99
|
+
f"output.{output_name}.mode.{mode['id']}",
|
|
100
|
+
f"output.{output_name}.hdr.{'enable' if hdr else 'disable'}",
|
|
101
|
+
]
|
|
102
|
+
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Common type definitions for monitor and screen information."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MonitorMode(TypedDict):
|
|
10
|
+
"""Generic abstraction for a monitor mode."""
|
|
11
|
+
|
|
12
|
+
id: str | None
|
|
13
|
+
width: int
|
|
14
|
+
height: int
|
|
15
|
+
fps: float
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MonitorInfo(TypedDict):
|
|
19
|
+
"""Generic a abstraction of a monitor."""
|
|
20
|
+
|
|
21
|
+
output_name: str
|
|
22
|
+
hdr: bool
|
|
23
|
+
modes: list[MonitorMode]
|
|
24
|
+
current_mode: MonitorMode
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ScreenSize(TypedDict):
|
|
28
|
+
"""Size of a screen."""
|
|
29
|
+
|
|
30
|
+
width: int
|
|
31
|
+
height: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ResolutionManager:
|
|
35
|
+
"""
|
|
36
|
+
Base class for managing display resolution and HDR settings.
|
|
37
|
+
|
|
38
|
+
This class serves as a blueprint for platform-specific resolution managers. Subclasses
|
|
39
|
+
must implement `query_monitor_info` and `apply_mode` to interface with the display server.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
client_width: int,
|
|
45
|
+
client_height: int,
|
|
46
|
+
client_fps: int,
|
|
47
|
+
client_hdr: bool = False,
|
|
48
|
+
) -> None:
|
|
49
|
+
self.client_width: int = client_width
|
|
50
|
+
self.client_height: int = client_height
|
|
51
|
+
self.client_fps: int = client_fps
|
|
52
|
+
self.client_hdr: bool = client_hdr
|
|
53
|
+
self.last_mode: Path = Path("~/.config/sunshine/last_mode.json").expanduser()
|
|
54
|
+
|
|
55
|
+
def query_monitor_info(self) -> MonitorInfo: # pragma: no cover
|
|
56
|
+
raise NotImplementedError()
|
|
57
|
+
|
|
58
|
+
def apply_mode(
|
|
59
|
+
self, output_name: str, mode: MonitorMode, hdr: bool = False
|
|
60
|
+
) -> None: # pragma: no cover
|
|
61
|
+
raise NotImplementedError()
|
|
62
|
+
|
|
63
|
+
def do(self) -> None:
|
|
64
|
+
monitor_info = self.query_monitor_info()
|
|
65
|
+
if (
|
|
66
|
+
monitor_info["current_mode"]["width"],
|
|
67
|
+
monitor_info["current_mode"]["height"],
|
|
68
|
+
) == (self.client_width, self.client_height):
|
|
69
|
+
print("Resolution already matches.")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
# Filter modes to matching resolution
|
|
73
|
+
matched_modes = [
|
|
74
|
+
mode
|
|
75
|
+
for mode in monitor_info["modes"]
|
|
76
|
+
if (
|
|
77
|
+
mode["height"] == self.client_height
|
|
78
|
+
and mode["width"] == self.client_width
|
|
79
|
+
)
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Sort by fps
|
|
83
|
+
matched_modes.sort(key=lambda m: m["fps"])
|
|
84
|
+
|
|
85
|
+
if not matched_modes:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Did not find mode matching {self.client_width}x{self.client_height} at {monitor_info['output_name']}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Get the mode with the closest refreshrate but not below
|
|
91
|
+
# Eg. if 25 is requested, and 20 and 30 are offered, return 30
|
|
92
|
+
select_mode: MonitorMode = matched_modes[-1]
|
|
93
|
+
for mode in matched_modes:
|
|
94
|
+
select_mode = mode
|
|
95
|
+
if mode["fps"] >= self.client_fps:
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
self.apply_mode(monitor_info["output_name"], select_mode, hdr=self.client_hdr)
|
|
99
|
+
|
|
100
|
+
# Save original monitor info to file
|
|
101
|
+
if not self.last_mode.parent.exists():
|
|
102
|
+
self.last_mode.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
|
|
104
|
+
_ = self.last_mode.write_text(json.dumps(monitor_info))
|
|
105
|
+
|
|
106
|
+
def undo(self) -> None:
|
|
107
|
+
# Check previous mode
|
|
108
|
+
if not self.last_mode.exists():
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
info: MonitorInfo = cast(MonitorInfo, json.loads(self.last_mode.read_text()))
|
|
112
|
+
|
|
113
|
+
# Set the new display mode
|
|
114
|
+
self.apply_mode(info["output_name"], info["current_mode"], hdr=info["hdr"])
|
|
115
|
+
|
|
116
|
+
# Clean up file
|
|
117
|
+
self.last_mode.unlink()
|
|
118
|
+
|
|
119
|
+
def toggle(self) -> None:
|
|
120
|
+
if self.last_mode.exists():
|
|
121
|
+
self.undo()
|
|
122
|
+
else:
|
|
123
|
+
self.do()
|