Skip to content

Commit aa33c6a

Browse files
committed
+ Run at startup option
1 parent a7b3146 commit aa33c6a

9 files changed

Lines changed: 165 additions & 43 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,4 @@ $RECYCLE.BIN/
264264
/settings.yaml
265265
/inversion_rules.yaml
266266
/color_filters.yaml
267+
/StartupTask.xml

CHANGELOG.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# Changelog
22
## [Unreleased](https://github.com/MaxBQb/InversionFilterManager/releases/tag/latest) (2022-07-29)
33
Features:
4+
- Option to request administrative privileges
5+
- Option to run app at system startup
46
- Single-app-instance check can be disabled with `--allow-multiple`/`-m` start param
57

6-
Stability:
7-
- Drop support of non-privileged run (Requires run as Administrator)
8-
98
## [Release v0.6.0](https://github.com/MaxBQb/InversionFilterManager/releases/tag/v0.6.0) (2022-07-29)
109
Features:
1110
- Use independent color filter

StartupTaskTemplate.xml

3.18 KB
Binary file not shown.

_meta.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
import os
23
from enum import Enum, auto
34

45

@@ -16,10 +17,5 @@ class IndirectDependency(Enum):
1617
CARRYON_BEFORE_UPDATE = auto()
1718

1819

19-
def get_app_dir():
20-
import os
21-
return os.path.dirname(os.path.abspath(sys.argv[0]))
22-
23-
24-
APP_DIR = get_app_dir()
25-
del get_app_dir
20+
APP_PATH = os.path.abspath(sys.argv[0])
21+
APP_DIR = os.path.dirname(APP_PATH)

manifest.xml

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
12
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
2-
<asmv3:trustInfo xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
3-
<asmv3:security>
4-
<asmv3:requestedPrivileges>
5-
<asmv3:requestedExecutionLevel
6-
level="requireAdministrator"
3+
<assemblyIdentity version="1.0.0.0"
4+
processorArchitecture="X86"
5+
name="InversionManager"
6+
type="win32"/>
7+
<description>Inverts colors when you opens blinding white windows</description>
8+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
9+
<security>
10+
<requestedPrivileges>
11+
<requestedExecutionLevel
12+
level="highestAvailable"
713
uiAccess="false" />
8-
</asmv3:requestedPrivileges>
9-
</asmv3:security>
10-
</asmv3:trustInfo>
14+
</requestedPrivileges>
15+
</security>
16+
</trustInfo>
1117
</assembly>

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
version=app.__version__,
1919
author=app.__author__,
2020
description="Inverts colors when you opens blinding white windows",
21-
data_files=[('.', ["update.bat"]),
21+
data_files=[('.', ["update.bat", "StartupTaskTemplate.xml"]),
2222
('./img', glob('img/*'))],
2323
options=dict(
2424
py2exe=dict(

tray/features.py

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
import ctypes
2+
import datetime
3+
import os.path
4+
import sys
5+
import textwrap
26

7+
import inject
38
import win32con
4-
import win32gui
59
import win32console
10+
import win32gui
11+
import win32security
12+
13+
import _meta
14+
15+
_shell = ctypes.windll.shell32
616

717

818
class Console:
@@ -19,7 +29,7 @@ def visible(self, value):
1929
self._visible = value
2030
win32gui.ShowWindow(
2131
self.handle,
22-
win32con.SW_SHOW if value else win32con.SW_HIDE
32+
self.get_visibility_code(value)
2333
)
2434

2535
def hide(self):
@@ -28,6 +38,107 @@ def hide(self):
2838
def show(self):
2939
self.visible = True
3040

41+
@staticmethod
42+
def get_visibility_code(show_console: bool):
43+
return win32con.SW_SHOW if show_console else win32con.SW_HIDE
44+
45+
46+
def has_admin_rights():
47+
return _shell.IsUserAnAdmin()
48+
49+
50+
class StartupTaskGenerator:
51+
def __init__(self,):
52+
self.script_path = str(_meta.APP_PATH)
53+
self.task_name = _meta.__product_name__ + "_Startup"
54+
self.template_path = os.path.abspath("StartupTaskTemplate.xml")
55+
self.result_path = os.path.abspath("StartupTask.xml")
56+
57+
def build_task(self):
58+
encoding = 'utf-16le'
59+
with open(self.template_path, encoding=encoding) as f:
60+
template_content = f.read()
61+
template_content = template_content.format(
62+
script_path=self.script_path,
63+
user_id=self.user_id,
64+
author=self.author,
65+
task_name=self.task_name,
66+
date_now=self.date_now,
67+
description=self.description
68+
)
69+
with open(self.result_path, 'w', encoding=encoding) as f:
70+
f.write(template_content)
71+
72+
@property
73+
def date_now(self):
74+
return datetime.datetime.now().isoformat()
75+
76+
@property
77+
def description(self):
78+
description = f"""
79+
Starts {_meta.__product_name__} at system start,
80+
use this app to automatically toggle color inversion
81+
on blinding-white windows occurs.
82+
This task generated by {_meta.__product_name__} v{_meta.__version__}
83+
(author {_meta.__author__})
84+
"""
85+
description = textwrap.dedent(description).strip()
86+
description = description.replace('\n', ' \n')
87+
return description
88+
89+
@property
90+
def author(self):
91+
return f"{os.environ['userdomain']}\\{os.environ['username']}"
92+
93+
@property
94+
def user_id(self) -> str:
95+
security_descriptor = win32security.GetFileSecurity(
96+
".", win32security.OWNER_SECURITY_INFORMATION
97+
)
98+
sid = security_descriptor.GetSecurityDescriptorOwner()
99+
return win32security.ConvertSidToStringSid(sid)
100+
101+
102+
class SystemStartupHandler:
103+
_TASK_COMMAND = "schtasks {args} > nul 2> nul"
104+
_TASK_CREATE_COMMAND = _TASK_COMMAND.format(args="/Create /XML \"{path}\" /TN \"{name}\"")
105+
_TASK_DELETE_COMMAND = _TASK_COMMAND.format(args="/Delete /F /TN \"{name}\"")
106+
_TASK_QUERY_COMMAND = _TASK_COMMAND.format(args="/Query /TN \"{name}\"")
107+
task_file = inject.attr(StartupTaskGenerator)
108+
109+
def subscribe(self):
110+
self.task_file.build_task()
111+
os.system(self._TASK_CREATE_COMMAND.format(
112+
path=self.task_file.result_path,
113+
name=self.task_file.task_name
114+
))
115+
116+
def unsubscribe(self):
117+
os.system(self._TASK_DELETE_COMMAND.format(
118+
name=self.task_file.task_name
119+
))
120+
121+
@property
122+
def is_subscribed(self):
123+
error_code = os.system(self._TASK_QUERY_COMMAND.format(
124+
name=self.task_file.task_name
125+
))
126+
return error_code == 0
127+
128+
@is_subscribed.setter
129+
def is_subscribed(self, value):
130+
if value:
131+
self.subscribe()
132+
else:
133+
self.unsubscribe()
134+
31135

32-
def is_admin():
33-
return ctypes.windll.shell32.IsUserAnAdmin()
136+
def start_with_admin_rights(show_console=False):
137+
# https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx
138+
if has_admin_rights():
139+
return
140+
return_code = _shell.ShellExecuteW(
141+
None, 'runas', sys.executable, sys.argv[0]+' -m', None,
142+
Console.get_visibility_code(show_console)
143+
)
144+
return return_code > 32

tray/tray.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from interaction import InteractionManager
1414
from inversion_rules import InversionRulesController
1515
from settings import UserSettingsController, OPTION_PATH, OPTION_CHANGE_HANDLER, T
16-
from tray.features import is_admin, Console
16+
from tray.features import has_admin_rights, Console, SystemStartupHandler, start_with_admin_rights
1717
from tray.utils import ref, make_toggle, make_radiobutton
1818
from utils import explore, app_abs_path, show_exceptions
1919

@@ -26,6 +26,7 @@ class Tray:
2626
updater = inject.attr(AutoUpdater)
2727
close_manager = inject.attr(AppCloseManager)
2828
console = inject.attr(Console)
29+
startup_handler = inject.attr(SystemStartupHandler)
2930

3031
def __init__(self):
3132
self.tray = None
@@ -67,17 +68,26 @@ def _open(path):
6768
lambda settings: settings.win_tracker.mode,
6869
change_mode_setter
6970
)
71+
is_admin = has_admin_rights()
7072

7173
im = self.im
7274
return Menu(
7375
MenuItem(
7476
f'{app.__product_name__} v{app.__version__}'
75-
+ (" (Admin)" if is_admin() else ""),
77+
+ (" (Admin)" if is_admin else ""),
7678
None, enabled=False),
79+
MenuItem("Re-Run as admin", self.restart_with_admin_rights,
80+
visible=(not is_admin)),
7781
Menu.SEPARATOR,
7882
MenuItem(
7983
ref("Show console"),
80-
*self.toggle_console()
84+
*make_toggle(self.toggle_console)
85+
),
86+
MenuItem(
87+
"Run a"+ref("t")+" system startup",
88+
*make_toggle(self.toggle_run_at_startup,
89+
self.startup_handler.is_subscribed),
90+
enabled=is_admin
8191
),
8292
MenuItem(
8393
ref("Mode"),
@@ -139,14 +149,20 @@ def new_handler(value: T):
139149
path, new_handler, True
140150
)
141151

142-
@make_toggle
143152
def toggle_console(self, value):
144153
self.console.visible = value
145154

155+
def toggle_run_at_startup(self, value):
156+
self.startup_handler.is_subscribed = value
157+
146158
@make_radiobutton({
147159
AppMode.DISABLE: ref("Ignore All"),
148160
AppMode.RULES: ref("According with rules"),
149161
}, AppMode.RULES)
150162
def change_mode(self, value: AppMode):
151163
self.settings_controller.settings.win_tracker.mode = value
152164
self.settings_controller.save()
165+
166+
def restart_with_admin_rights(self):
167+
if start_with_admin_rights(self.console.visible):
168+
self.close_manager.close()

tray/utils.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,21 @@ def ref(text: str):
1313
return f'&{text[0]}\u0332{text[1:]}'
1414

1515

16-
def make_toggle(out_func=None, default_value=False):
17-
def decorator(func):
18-
def wrapper(self):
19-
value = [default_value]
16+
def make_toggle(setter=None, default_value=False):
17+
value = [default_value]
2018

21-
def get_value(item):
22-
return value[0]
19+
def get_value(item):
20+
return value[0]
2321

24-
def toggle():
25-
value[0] ^= True
26-
func(self, value[0])
22+
def toggle():
23+
value[0] ^= True
24+
setter(value[0])
2725

28-
return toggle, get_value
29-
return wrapper
30-
if out_func:
31-
return decorator(out_func)
32-
return decorator
26+
return toggle, get_value
3327

3428

3529
def make_radiobutton(values: dict[Hashable, str],
36-
default_value=None,
37-
):
30+
default_value=None):
3831
def decorator(func):
3932
def wrapper(*args, **kwargs):
4033
value_ref = [default_value or next(iter(values))]

0 commit comments

Comments
 (0)