dotfiles/dot_config/i3/executable_monitor_manager
2023-01-30 14:28:59 +01:00

728 lines
27 KiB
Python

#!/usr/bin/env python3
#
# Copyright (c) 2016 James Murphy
# Licensed under the GPL version 2 only
#
# monitor_manager is an i3blocks blocklet script to quickly manage your
# connected output devices
from tkinter import *
from tkinter import messagebox
from shutil import which
import tkinter.font as font
from subprocess import call, check_output, CalledProcessError
import re
import os
DESKTOP_SYMBOL = "\uf108"
UP_ARROW = "\uf062"
DOWN_ARROW = "\uf063"
UNBLANKED_SYMBOL = "\uf06e"
BLANKED_SYMBOL = "\uf070"
NOT_CLONED_SYMBOL = "\uf096"
PRIMARY_SYMBOL = "\uf005"
SECONDARY_SYMBOL = "\uf006"
CLONED_SYMBOL = "\uf24d"
ROTATION_NORMAL = "\uf151"
ROTATION_LEFT = "\uf191"
ROTATION_RIGHT = "\uf152"
ROTATION_INVERTED = "\uf150"
REFLECTION_NORMAL = "\uf176"
REFLECTION_X = "\uf07e"
REFLECTION_Y = "\uf07d"
REFLECTION_XY = "\uf047"
TOGGLE_ON = "\uf205"
TOGGLE_OFF = "\uf204"
APPLY_SYMBOL = "\uf00c"
CANCEL_SYMBOL = "\uf00d"
ARANDR_SYMBOL = "\uf085"
REFRESH_SYMBOL = "\uf021"
strbool = lambda s: s.lower() in ['t', 'true', '1']
def _default(name, default='', arg_type=strbool):
val = default
if name in os.environ:
val = os.environ[name]
return arg_type(val)
SHOW_ON_OFF = _default("SHOW_ON_OFF","1")
SHOW_NAMES = _default("SHOW_NAMES", "1")
SHOW_PRIMARY = _default("SHOW_PRIMARY", "1")
SHOW_MODE = _default("SHOW_MODE", "1")
SHOW_BLANKED = _default("SHOW_BLANKED", "1")
SHOW_DUPLICATE = _default("SHOW_DUPLICATE", "1")
SHOW_ROTATION = _default("SHOW_ROTATION", "1")
SHOW_REFLECTION = _default("SHOW_REFLECTION", "1")
SHOW_BRIGHTNESS = _default("SHOW_BRIGHTNESS", "1")
SHOW_BRIGHTNESS_VALUE = _default("SHOW_BRIGHTNESS_VALUE", "0")
SHOW_UP_DOWN = _default("SHOW_UP_DOWN", "1")
FONTAWESOME_FONT_FAMILY = "FontAwesome"
FONTAWESOME_FONT_SIZE = 11
FONTAWESOME_FONT = (FONTAWESOME_FONT_FAMILY, FONTAWESOME_FONT_SIZE)
DEFAULT_FONT_FAMILY = _default("FONT_FAMILY","DejaVu Sans Mono", str)
DEFAULT_FONT_SIZE = _default("FONT_SIZE", 11, int)
DEFAULT_FONT = (DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE)
BRIGHTNESS_SLIDER_HANDLE_LENGTH = 20
BRIGHTNESS_SLIDER_WIDTH = 15
BRIGHTNESS_SLIDER_LENGTH = 50
WINDOW_CLOSE_TO_BOUNDARY_BUFFER = _default("CLOSE_TO_BOUNDARY_BUFFER", 20, int)
class Output:
def __init__(self, name=None, w=None, h=None, x=None, y=None, rate=None,
active=False, primary=False, sameAs=None, blanked=False, rotation="normal",
reflection="normal", brightness=1.0):
self.name = name
self.w = w
self.h = h
self.x = x
self.y = y
self.rate = rate
self.active = active
self.primary = primary
self.sameAs = sameAs
self.blanked = blanked
self.modes = []
self.currentModeIndex = None
self.preferredModeIndex = None
self.row = None
self.rotation = rotation
self.reflection = reflection
self.brightness = brightness
def setPreferredMode(self):
if self.preferredModeIndex != None:
self.setMode(self.preferredModeIndex)
elif self.modes != None:
self.setMode(0)
def setMode(self, index):
self.w, self.h, self.rate = self.modes[index]
self.currentModeIndex = index
def realOutputs():
outputs = []
xrandrText = check_output(["xrandr","--verbose"],universal_newlines=True)
outputBlocks = re.split(r'\n(?=\S)', xrandrText, re.MULTILINE)
infoPattern = re.compile(
r'^(\S+)' # output name
' connected ' # must be connected
'(primary )?' # check if primary output
'((\d+)x(\d+)\+(\d+)\+(\d+) )?' # width x height + xoffset + yoffset
'(\(\S+\) )?' # mode code (0x4a)
'(normal|left|inverted|right)? ?' # rotation
'(X axis|Y axis|X and Y axis)?') # reflection
brightnessPattern = re.compile(r'^\tBrightness: ([\d.]+)', re.MULTILINE)
modePattern = re.compile(r'^ (\d+)x(\d+)[^\n]*?\n +h:[^\n]*?\n +v:[^\n]*?([\d.]+)Hz$', re.MULTILINE)
for outputBlock in outputBlocks:
output = Output()
infoMatch = infoPattern.match(outputBlock)
if infoMatch:
output.name = infoMatch.group(1)
if infoMatch.group(2):
output.primary = True
if infoMatch.group(3):
output.active = True
output.w, output.h, output.x, output.y = map(int,infoMatch.group(4, 5, 6, 7))
if infoMatch.group(9):
output.rotation = infoMatch.group(9)
if output.rotation in ["left", "right"]:
output.w, output.h = output.h, output.w
if infoMatch.group(10):
if infoMatch.group(10) == "X axis":
output.reflection = "x"
elif infoMatch.group(10) == "Y axis":
output.reflection = "y"
elif infoMatch.group(10) == "X and Y axis":
output.reflection = "xy"
else:
output.reflection = "normal"
else:
output.reflection = "normal"
brightnessMatch = brightnessPattern.search(outputBlock)
if brightnessMatch:
try:
brightness = float(brightnessMatch.group(1))
output.brightness = brightness
if abs(brightness) < 1e-09:
output.blanked = True
except ValueError:
pass
modeMatches = modePattern.finditer(outputBlock)
for i, modeMatch in enumerate(modeMatches):
if "*current" in modeMatch.group(0):
output.currentModeIndex = i
output.rate = modeMatch.group(3)
if "+preferred" in modeMatch.group(0):
output.preferredModeIndex = i
output.modes.append(modeMatch.group(1,2,3))
outputs.append(output)
outputs.sort(key=lambda m: m.x if m.x != None else -1)
prev = None
for output in outputs:
if prev != None and output.active and prev.active and output.x == prev.x:
output.sameAs = prev.name
else:
prev = output
return outputs
def modestr(mode):
return "{}x{}@{}".format(*mode)
def status(self):
if self.active:
if self.sameAs == None or self.sameAs == self.name:
if self.w and self.h and self.rate:
return "{}x{}@{}".format(self.w, self.h, self.rate)
else:
return "auto"
else:
return "duplicate {}".format(self.sameAs)
else:
return "Inactive"
def __str__(self):
return "{} {}x{}+{}+{} active:{}, primary:{}\nmodes:{}\ncurrentIndex:{} preferredIndex:{}".format(
self.name, self.w, self.h, self.x, self.y, self.active, self.primary, self.modes, self.currentModeIndex, self.preferredModeIndex)
class MonitorManager():
def __init__(self, root):
self.root = root
self.root.withdraw()
self.root.resizable(0,0)
self.root.wm_title("Monitor Manager")
self.frame = None
self.outputs = []
self.hardRefreshList()
style = {'relief':FLAT, 'padx':1, 'pady':1, 'anchor':'w', 'font':FONTAWESOME_FONT}
self.infoLabel = Label(self.root, text="", **style)
self.infoLabel.config(font=DEFAULT_FONT)
self.bottomRow = []
self.applyButton = Button(self.root, text=APPLY_SYMBOL, **style)
self.bottomRow.append(self.applyButton)
self.refreshButton = Button(self.root, text=REFRESH_SYMBOL, **style)
self.bottomRow.append(self.refreshButton)
if which("arandr"):
self.arandrButton = Button(self.root, text=ARANDR_SYMBOL, **style)
self.bottomRow.append(self.arandrButton)
else:
self.arandrButton = None
self.cancelButton = Button(self.root, text=CANCEL_SYMBOL, **style)
self.bottomRow.append(self.cancelButton)
self.infoLabel.grid(row=1,column=0, columnspan=len(self.bottomRow))
self.gridRow(2, self.bottomRow)
self.moveToMouse()
self.root.deiconify()
def registerBindings(self):
self.root.bind("<Return>", self.handleApply)
self.root.bind("<Escape>", self.handleCancel)
self.applyButton.bind("<Button-1>", self.handleApply)
self.setInfo(self.applyButton, "Apply changes")
self.refreshButton.bind("<Button-1>", self.hardRefreshList)
self.setInfo(self.refreshButton, "Refresh list")
if self.arandrButton:
self.arandrButton.bind("<Button-1>", self.handleArandr)
self.setInfo(self.arandrButton, "Launch aRandR")
self.cancelButton.bind("<Button-1>", self.handleCancel)
self.setInfo(self.cancelButton, "Cancel")
for toggleButton in self.toggleButtons:
toggleButton.bind("<Button-1>", self.toggleActive)
toggleButton.bind("<Button-4>", self.handleUp)
toggleButton.bind("<Button-5>", self.handleDown)
self.setInfo(toggleButton, "Turn output on/off")
for primaryButton in self.primaryButtons:
primaryButton.bind("<Button-1>", self.setPrimary)
self.setInfo(primaryButton, "Set primary output")
for blankedButton in self.blankedButtons:
blankedButton.bind("<Button-1>", self.toggleBlanked)
self.setInfo(blankedButton, "Show/hide output")
for duplicateButton in self.duplicateButtons:
duplicateButton.bind("<Button-1>", self.toggleDuplicate)
self.setInfo(duplicateButton, "Duplicate another output")
for rotateButton in self.rotateButtons:
rotateButton.bind("<Button-1>", self.cycleRotation)
self.setInfo(rotateButton, "Rotate output")
for reflectButton in self.reflectButtons:
reflectButton.bind("<Button-1>", self.cycleReflection)
self.setInfo(reflectButton, "Reflect output")
for brightnessSlider in self.brightnessSliders:
brightnessSlider.bind("<ButtonRelease-1>", self.updateBrightness)
self.setInfo(brightnessSlider, "Adjust brightness")
for upButton in self.upButtons:
upButton.bind("<Button-1>", self.handleUp)
upButton.bind("<Button-4>", self.handleUp)
upButton.bind("<Button-5>", self.handleDown)
self.setInfo(upButton, "Move up")
for downButton in self.downButtons:
downButton.bind("<Button-1>", self.handleDown)
downButton.bind("<Button-4>", self.handleUp)
downButton.bind("<Button-5>", self.handleDown)
self.setInfo(downButton, "Move down")
def gridRow(self, row, widgets):
column = 0
for w in widgets:
w.grid(row=row, column=column)
column += 1
def moveToMouse(self):
root = self.root
root.update_idletasks()
width = root.winfo_reqwidth()
height = root.winfo_reqheight()
x = root.winfo_pointerx() - width//2
y = root.winfo_pointery() - height//2
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
if x+width > screen_width - WINDOW_CLOSE_TO_BOUNDARY_BUFFER:
x = screen_width - WINDOW_CLOSE_TO_BOUNDARY_BUFFER - width
elif x < WINDOW_CLOSE_TO_BOUNDARY_BUFFER:
x = WINDOW_CLOSE_TO_BOUNDARY_BUFFER
if y+height > screen_height - WINDOW_CLOSE_TO_BOUNDARY_BUFFER:
y = screen_height - WINDOW_CLOSE_TO_BOUNDARY_BUFFER - height
elif y < WINDOW_CLOSE_TO_BOUNDARY_BUFFER:
y = WINDOW_CLOSE_TO_BOUNDARY_BUFFER
root.geometry('+{}+{}'.format(x, y))
def setInfo(self, widget, info):
widget.bind("<Enter>", lambda e: self.infoLabel.config(text=info, fg="black"))
widget.bind("<Leave>", lambda e: self.infoLabel.config(text=""))
def handleApply(self, e=None):
self.root.after_idle(self.doHandleApply)
def doHandleApply(self):
if not self.getUserConfirmationIfDangerousConfiguration():
return
command = ["xrandr"]
if not self.existsPrimary():
command += ["--noprimary"]
partition = self.sameAsPartition()
prevFirstActive = None
for p in partition:
firstActive = None
for output in p:
command += ["--output", output.name]
if output.active:
if firstActive == None:
firstActive = output
else:
command += ["--same-as", firstActive.name]
if output.primary:
command += ["--primary"]
if prevFirstActive != None:
command += ["--right-of", prevFirstActive.name]
if output.w != None and output.h != None and output.rate != None:
command += ["--mode", "{}x{}".format(output.w,output.h)]
command += ["--rate", output.rate ]
else:
command += ["--auto"]
command += ["--brightness", str(output.brightness)]
command += ["--rotate", output.rotation]
command += ["--reflect", output.reflection]
else:
command += ["--off"]
if firstActive:
prevFirstActive = firstActive
self.root.after_idle(lambda: self.executeXrandrCommand(command))
def executeXrandrCommand(self, command):
try:
check_output(command, universal_newlines=True)
except CalledProcessError as err:
self.infoLabel.config(text="xrandr returned nonzero exit status {}".format(err.returncode), fg="red")
def getUserConfirmationIfDangerousConfiguration(self):
result = "yes"
if all(map(lambda o: o.blanked or not o.active, self.outputs)):
result = messagebox.askquestion("All blanked or off",
"All ouputs are set to be turned off or blanked, continue?",
icon="warning")
return result == "yes"
def sameAsPartition(self):
partition = []
for output in self.outputs:
place = None
for p in partition:
if place != None:
break;
for o in p:
if place == None and (output.sameAs == o.name or o.sameAs == output.name):
place = p
break;
if place:
place.append(output)
else:
partition.append([output])
return partition
def handleCancel(self, e=None):
self.root.destroy()
def handleArandr(self, e=None):
call(["i3-msg", "-q", "exec", "arandr"])
self.root.destroy()
def handleUp(self, e):
row = e.widget.output.row
if row > 0:
self.swapOutputRows(row-1, row)
self.softRefreshList()
def handleDown(self, e):
row = e.widget.output.row
n = len(self.outputs)
if row + 1 < n:
self.swapOutputRows(row, row+1)
self.softRefreshList()
def swapOutputRows(self, row1, row2):
outputs = self.outputs
outputs[row1],outputs[row2] = outputs[row2],outputs[row1]
outputs[row1].row = row1
outputs[row2].row = row2
for widget in self.frame.grid_slaves(row=row2):
widget.output = outputs[row2]
for widget in self.frame.grid_slaves(row=row1):
widget.output = outputs[row1]
def setPrimary(self, e):
output = e.widget.output
output.primary = not output.primary
for otherOutput in self.outputs:
if otherOutput != output:
otherOutput.primary = False
self.softRefreshList()
def existsPrimary(self):
for output in self.outputs:
if output.primary:
return True
return False
def toggleActive(self, e):
output = e.widget.output
output.active = not output.active
if output.active:
output.setPreferredMode()
else:
for otherOutput in self.outputs:
if otherOutput.sameAs == output.name:
otherOutput.sameAs = None
self.softRefreshList()
def toggleBlanked(self, e):
output = e.widget.output
if output.blanked:
output.blanked = False
output.brightness = 1.0
else:
output.blanked = True
output.brightness = 0.0
self.softRefreshList()
def updateBrightness(self, e):
output = e.widget.output
output.brightness = .01 * e.widget.get()
output.blanked = False
if abs(output.brightness) < 1e-09:
output.blanked = True
self.softRefreshList()
def cycleRotation(self, e):
output = e.widget.output
if output.rotation == "normal":
output.rotation = "right"
elif output.rotation == "right":
output.rotation = "inverted"
elif output.rotation == "inverted":
output.rotation = "left"
else:
output.rotation = "normal"
self.softRefreshList()
def rotationSymbol(self, rotation):
return {
"normal": ROTATION_NORMAL,
"left": ROTATION_LEFT,
"right": ROTATION_RIGHT,
"inverted": ROTATION_INVERTED,
}[rotation]
def cycleReflection(self, e):
output = e.widget.output
if output.reflection == "normal":
output.reflection = "x"
elif output.reflection == "x":
output.reflection = "y"
elif output.reflection == "y":
output.reflection = "xy"
else:
output.reflection = "normal"
self.softRefreshList()
def reflectionSymbol(self, reflection):
return {
"normal": REFLECTION_NORMAL,
"x": REFLECTION_X,
"y": REFLECTION_Y,
"xy": REFLECTION_XY,
}[reflection]
def toggleDuplicate(self, e):
duplicateButton = e.widget
optionMenu = duplicateButton.statusOptionMenu
output = optionMenu.output
if output.sameAs != None:
output.sameAs = None
self.setMenuToOutput(optionMenu, output)
else:
self.setMenuToDuplicate(optionMenu)
self.softRefreshList()
def getDuplicableOutputsFor(self, output):
return [o for o in self.outputs if o != output and o.sameAs == None]
def softRefreshList(self, e=None):
for widget in set().union(self.nameLabels, self.primaryButtons,
self.statusOptionMenus, self.blankedButtons,
self.duplicateButtons, self.rotateButtons, self.reflectButtons,
self.brightnessSliders, self.upButtons, self.downButtons):
widget.config(fg="gray" if not widget.output.active else "black")
for widget in self.toggleButtons:
widget.config(text=TOGGLE_ON if widget.output.active else TOGGLE_OFF)
for widget in self.nameLabels:
widget.config(text=widget.output.name)
for widget in self.primaryButtons:
widget.config(text=PRIMARY_SYMBOL if widget.output.primary else SECONDARY_SYMBOL)
if not widget.output.primary:
widget.config(fg="gray")
for widget in self.statusOptionMenus:
widget.config(text=widget.output.status())
if widget.output.sameAs != None:
self.setMenuToDuplicate(widget)
self.setInfo(widget, "Select output to duplicate")
else:
self.setMenuToOutput(widget, widget.output)
self.setInfo(widget, "Select output mode")
for widget in self.blankedButtons:
widget.config(text=BLANKED_SYMBOL if widget.output.blanked else UNBLANKED_SYMBOL)
for widget in self.duplicateButtons:
widget.config(text=CLONED_SYMBOL if widget.output.sameAs else NOT_CLONED_SYMBOL)
for widget in self.rotateButtons:
widget.config(text=self.rotationSymbol(widget.output.rotation))
for widget in self.reflectButtons:
widget.config(text=self.reflectionSymbol(widget.output.reflection))
for widget in self.brightnessSliders:
widget.set(int(100*widget.output.brightness))
def hardRefreshList(self, e=None):
self.outputs = Output.realOutputs()
self.root.after_idle(self.populateGrid)
def populateGrid(self):
oldFrame = self.frame
self.frame = Frame(self.root)
self.frame.grid(row=0, column=0, columnspan=len(self.bottomRow))
self.toggleButtons = []
self.nameLabels = []
self.primaryButtons = []
self.statusOptionMenus = []
self.blankedButtons = []
self.duplicateButtons = []
self.rotateButtons = []
self.reflectButtons = []
self.brightnessSliders = []
self.upButtons = []
self.downButtons = []
for row, output in enumerate(self.outputs):
self.makeLabelRow(output, row)
self.registerBindings()
if oldFrame:
oldFrame.destroy()
def makeLabelRow(self, output, row):
output.row = row
style = {'relief':FLAT, 'padx':1, 'pady':1, 'anchor':'w'}
widgets = []
toggleButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
toggleButton.output = output
self.toggleButtons.append(toggleButton)
if SHOW_ON_OFF:
widgets.append(toggleButton)
nameLabel = Label(self.frame, font=DEFAULT_FONT)
nameLabel.output = output
self.nameLabels.append(nameLabel)
if SHOW_NAMES:
widgets.append(nameLabel)
primaryButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
primaryButton.output = output
self.primaryButtons.append(primaryButton)
if not output.primary:
primaryButton.config(fg="gray")
if SHOW_PRIMARY:
widgets.append(primaryButton)
var = StringVar(self.frame)
statusOptionMenu = OptionMenu(self.frame, var, None)
statusOptionMenu.output = output
statusOptionMenu.var = var
statusOptionMenu.config(relief=FLAT)
self.statusOptionMenus.append(statusOptionMenu)
if SHOW_MODE or SHOW_DUPLICATE:
widgets.append(statusOptionMenu)
blankedButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
blankedButton.output = output
self.blankedButtons.append(blankedButton)
if SHOW_BLANKED:
widgets.append(blankedButton)
duplicateButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
duplicateButton.statusOptionMenu = statusOptionMenu
duplicateButton.output = output
self.duplicateButtons.append(duplicateButton)
if SHOW_DUPLICATE:
widgets.append(duplicateButton)
rotateButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
rotateButton.output = output
self.rotateButtons.append(rotateButton)
if SHOW_ROTATION:
widgets.append(rotateButton)
reflectButton = Button(self.frame, font=FONTAWESOME_FONT, **style)
reflectButton.output = output
self.reflectButtons.append(reflectButton)
if SHOW_REFLECTION:
widgets.append(reflectButton)
brightnessSlider = Scale(self.frame, orient="horizontal", from_=0, to=100,
length=BRIGHTNESS_SLIDER_LENGTH, showvalue=SHOW_BRIGHTNESS_VALUE,
sliderlength=BRIGHTNESS_SLIDER_HANDLE_LENGTH,
width=BRIGHTNESS_SLIDER_WIDTH, font=FONTAWESOME_FONT)
brightnessSlider.output = output
self.brightnessSliders.append(brightnessSlider)
if SHOW_BRIGHTNESS:
widgets.append(brightnessSlider)
upButton = Button(self.frame, text=UP_ARROW, font=FONTAWESOME_FONT, **style)
upButton.output = output
self.upButtons.append(upButton)
if SHOW_UP_DOWN:
widgets.append(upButton)
downButton = Button(self.frame, text=DOWN_ARROW, font=FONTAWESOME_FONT, **style)
downButton.output = output
self.downButtons.append(downButton)
if SHOW_UP_DOWN:
widgets.append(downButton)
for widget in widgets:
widget.output = output
self.gridRow(row, widgets)
self.softRefreshList()
def setMenuToOutput(self, optionMenu, output):
menu = optionMenu["menu"]
var = optionMenu.var
modes = output.modes
menu.delete(0, END)
for i, mode in enumerate(modes):
label = Output.modestr(mode)
menu.add_command(label=label, command=setLabelAndOutputModeFunc(var,label,output,i))
if output.currentModeIndex != None:
var.set(Output.modestr(modes[output.currentModeIndex]))
elif output.preferredModeIndex != None:
var.set(Output.modestr(modes[output.preferredModeIndex]))
elif len(modes) > 0:
var.set(Output.modestr(modes[0]))
def setMenuToDuplicate(self, optionMenu):
menu = optionMenu["menu"]
var = optionMenu.var
output = optionMenu.output
menu.delete(0, END)
duplicables = self.getDuplicableOutputsFor(output)
defaultIndex = 0
for i,otherOutput in enumerate(duplicables):
label = otherOutput.name
menu.add_command(label=label, command=setLabelAndSameAsFunc(var,label,output))
if label == output.sameAs:
defaultIndex = i
if len(duplicables) > 0:
var.set(menu.entrycget(defaultIndex, "label"))
output.sameAs = menu.entrycget(defaultIndex, "label")
else:
var.set("None")
def handleFocusOut(self, event):
self.root.destroy()
def setLabelAndOutputModeFunc(var, label, output, i):
def func():
var.set(label)
output.setMode(i)
return func
def setLabelAndSameAsFunc(var, sameAs, output):
def func():
var.set(sameAs)
output.sameAs = sameAs
return func
if os.environ.get('BLOCK_BUTTON') == "1":
if os.fork() != 0:
root = Tk()
if DEFAULT_FONT_FAMILY and DEFAULT_FONT_SIZE:
font.nametofont("TkDefaultFont").config(family=DEFAULT_FONT_FAMILY, size=DEFAULT_FONT_SIZE)
manager = MonitorManager(root)
root.mainloop()
else:
print(DESKTOP_SYMBOL)
else:
print(DESKTOP_SYMBOL)