Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
14c05d4
App correctly gets display names from display EDID and
Mar 22, 2024
a8be596
Remove extra print statement
Mar 22, 2024
a24580b
Added fallback to display connection name if
Mar 27, 2024
893ceaf
Add verbosity arguements for debugging without shoving things in cons…
Aug 1, 2024
db19306
Updated display validity check in init to match new display array set…
Aug 2, 2024
ad8e634
Display reordering is now directly after discovering displays to prev…
Aug 2, 2024
ab675fc
Fixed laptop brightness slider not being reenabled after leaving ddcu…
Aug 2, 2024
05d7555
Secondary Brightness slider enabled after leaving ddcutil mode
Aug 2, 2024
f890828
Fixed crashing on Wayland! Monitor names are missing now since xrandr…
Sep 8, 2024
d3a53af
The old software brightness setting is broken on wayland, but display…
Sep 8, 2024
a08d304
restored original poetry imports.
Sep 8, 2024
fd883f8
Added warning message for wayland users. I should probably
Sep 17, 2024
be90b30
Added checks to prevent laptop displays from being set. The sliders w…
Sep 17, 2024
798e157
added try block to xrandr execution as well.
Sep 17, 2024
d5e7597
Now forcing ddcutil on wayland.
Sep 17, 2024
2a39630
Fix pyproject using lowercase readme.
Sep 21, 2024
972f51a
allow PyQt5 to be imported instead if qtpy fails to import.
Sep 21, 2024
8cd05ff
Undo pyqt5 import
Sep 21, 2024
59061b6
Created logging functions.
Sep 22, 2024
6b25fee
Added logging EVERYWHERE
Sep 22, 2024
8f9aabc
Add extra check for user missing i2c group and alert user with instru…
Soggy-Pancake Mar 24, 2025
951d132
Add extra check for user missing i2c group and alert user with instru…
Soggy-Pancake Mar 24, 2025
0e909be
Merge branch 'master' of https://github.com/Soggy-Pancake/Brightness-…
Soggy-Pancake Mar 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,6 @@ ENV/
.mypy*

# idea
.idea*
.idea*
brightness-controller-linux/test.txt
brightness-controller-linux/testDdc.txt
310 changes: 202 additions & 108 deletions brightness-controller-linux/brightness_controller_linux/init.py

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@
# You should have received a copy of the GNU General Public License
# along with Brightness Controller. If not, see <http://www.gnu.org/licenses/>.

import subprocess
import subprocess, os
import shlex
import re

debug = False
try:
import brightness_controller_linux.util.log as log
except:
import log #used in testing
debug = True

def query_xrandr():
query = "xrandr --query"
Expand All @@ -38,6 +44,196 @@ def extract_displays(output):
return connected_displays


def extract_edid_name(edid_hex):
try:
display_name = edid_hex[edid_hex.find('fc00') + 4:]
display_name = display_name[:display_name.find('0a')]

return bytes.fromhex(display_name).decode()

except Exception as e:
print("Error:", e)
return None

def x11_Monitor_Name_Extractor(xrandr_monitors : list):

displays = []

for monitor in xrandr_monitors:
monitorInfo = []
gettingEDID = False
currentEdid = ""
for line in monitor:

if "connected" in line:
monitorInfo.append(line[:line.find(' ')])

if gettingEDID and line.startswith("\t\t"):
currentEdid += line[2:]
elif gettingEDID:
break
else:
gettingEDID = False

if line == "\tEDID: ":
gettingEDID = True


monitorName = extract_edid_name(currentEdid)
if monitorName:
monitorInfo.append(monitorName)
log.info(f"{monitor[0]} name is {monitorName}")
else:
print(f"Failed to get display name from monitor {monitor[0]}")
log.info(f"Failed to get display name from monitor {monitor[0]}")
monitorInfo.append(monitorInfo[0])

displays.append(monitorInfo)

log.info(f"[x11] Monitor names extracted: [{displays}]")

return displays

def wayland_Monitor_Info_Extractor(monitorInfo):

connectionName = None
modelName = None

for line in monitorInfo:
#print(line)

if line.startswith('\tname:'):
connectionName = line.split(':')[1].strip()

if line.startswith("\tmake:"):
model = line.split(',')[1]
modelName = model[ model.find("'") + 1 : model.rfind("/") ]
#print(modelName)

if modelName and connectionName:
return [connectionName, modelName]
else:
return None


def wayland_Monitor_Name_Extractor():

waylandInfo = []
displays = []
currentDisplay = []

try:
waylandInfo = subprocess.check_output(["wayland-info"]).decode().splitlines()
except:
print("ERROR: Please install the package \"wayland-utils\" for the wayland-info command!")
print("Monitor names will not be shown as they can't be labeled")
return None

i = -1
for line in waylandInfo:
i += 1

if line.startswith("interface: 'wl_output',") or i == len(waylandInfo) - 1:
if currentDisplay[0].startswith("interface: 'wl_output',"):
#get display info out
monitorInfo = wayland_Monitor_Info_Extractor(currentDisplay)
if not monitorInfo[0].startswith("Unknown"):
displays.append(monitorInfo)
#print(currentDisplay)
currentDisplay = []

currentDisplay.append(line)

log.info(f"[wayland] Monitor names extracted: {displays}")

return displays


def extract_display_names(testInfo = None):
xrandr_output = subprocess.check_output(["xrandr", "--verbose"]).decode().splitlines()

displayVerboseInfo = []
display = []

#get verbose data for displays
i = -1
for line in xrandr_output:
i += 1

if line.startswith("Screen"): continue

if i == len(xrandr_output) - 1:
display.append(line)
if "disconnected" not in display[0]:
if not display[0].startswith("Unknown"):
displayVerboseInfo.append(display)

if not line.startswith("\t") and "connected" in line:
if len(display) > 0:
if "disconnected" not in display[0]:
if not display[0].startswith("Unknown"):
displayVerboseInfo.append(display)

display = []
display.append(line)
else:
display.append(line)

log.info(f"Display info : {len(displayVerboseInfo)} displays.")
log.info(displayVerboseInfo)
log.info("")

if os.getenv("XDG_SESSION_TYPE") == "wayland":
waylandDisplayNames = wayland_Monitor_Name_Extractor()
if waylandDisplayNames == None:
# fall back to old nameing
log.warning("Fell back to x11 monitor name extraction!")
print("ERROR Falling back to x11 monitor name extractor! Names may not be extracted!")
return x11_Monitor_Name_Extractor(displayVerboseInfo)

# we got display names from wayland!
return waylandDisplayNames

else:
return x11_Monitor_Name_Extractor(displayVerboseInfo)


def match_ddc_order(monitorNames):

detectedMonitors = subprocess.check_output(["ddcutil", "detect"]).decode().splitlines()

log.info("ddcutil detect output:")
log.info(detectedMonitors)

reorderedMonitors = []

#laptopMonitorNames = [['eDP-1', 'eDP-1'], ['HDMI-1', 'VG279']]

#laptopTestCase = ['Display 1', ' I2C bus: /dev/i2c-1', ' EDID synopsis:', ' Mfg id: AUS', ' Model: VG279', ' Serial number: Redacted', ' Manufacture year: 2020', ' EDID version: 1.3', ' VCP version: 2.2', '', 'Invalid display', ' I2C bus: /dev/i2c-4', ' EDID synopsis:', ' Mfg id: BOE', ' Model: ', ' Serial number: ', ' Manufacture year: 2015', ' EDID version: 1.4', ' DDC communication failed', ' This is an eDP laptop display. Laptop displays do not support DDC/CI.', '']

for line in detectedMonitors:
if "Model" in line:
for monitor in monitorNames:
modelName = line.split(":")[1].strip()
log.info(f"[ddcReorder] Model name output {modelName}")
if modelName == '':
if monitor[1].startswith('eDP'):
reorderedMonitors.append(monitor)
log.info(f"[ddcReorder] added {monitor} from {modelName}")
break

if monitor[1] in modelName:
reorderedMonitors.append(monitor)
log.info(f"[ddcReorder] added {monitor} from {modelName}")
break

if len(monitorNames) != len(reorderedMonitors):
print(f"ERROR IN MONITOR REORDERING please create an issue on the github with your log file at ~/.config/brightness_controller/log.txt")
log.error(f"Failed attempting to reorder monitors, input: {monitorNames} attempted output: {reorderedMonitors}")
return monitorNames # fall back to unreordered output
return reorderedMonitors


def detect_display_devices():
"""
Detects available displays.
Expand All @@ -47,5 +243,9 @@ def detect_display_devices():
return extract_displays(query_xrandr())


if __name__ == '__main__':
print(detect_display_devices())
if debug:
#print(detect_display_devices())
with open("test.txt", 'r') as file:
testInfo = file.readlines()

extract_display_names(testInfo)
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,7 @@


def execute_command(string_cmd):
subprocess.check_output(string_cmd, shell=True)
try:
subprocess.check_output(string_cmd, shell=True)
except:
None
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-

# This file is part of Brightness Controller.
#
# Brightness Controller is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Brightness Controller is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Brightness Controller. If not, see
# <http://www.gnu.org/licenses/>.

import os, sys
from datetime import datetime

logPath = f"/home/{os.getlogin()}/.config/brightness_controller/log.txt"

def write(logString, level):
currentTime = datetime.now()
with open(logPath, 'a') as file:
if type(logString) is list:
for line in logString:
if type(line) is list:
write(line, level)
else:
file.write(f"[{currentTime}] [{level}] {line}\n")
else:
file.write(f"[{currentTime}] [{level}] {logString}\n")

def info(logString):
write(logString, "info")

def warning(logString):
write(logString, "warning")

def error(logString):
write(logString, "ERROR")

def fatal(logString):
write(logString, "FATAL")

def begin():
if not os.path.exists(logPath):
try:
os.makedirs(f"/home/{os.getlogin()}/.config/brightness_controller")
except:
None
open(logPath, 'w').close()

info("Application start!")
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from importlib import resources
from pathlib import Path

import brightness_controller_linux.icons as icons
import icons as icons


def icon_path(module_name=icons):
Expand Down
Loading