Skip to content

Commit 4cad5d5

Browse files
committed
refactor(utils): move and expand cli functionality from yasb_cli to cli
This commit moves the CLI functionality from `yasb_cli.py` to a new module `cli.py` under `core/utils/`. The `CLIHandler` class has been expanded with additional commands related to enabling and disabling autostart, and managing scheduled tasks. The `CustomArgumentParser` class has been added to handle custom error messages. Additionally, the code now supports enabling/disabling autostart as a scheduled task with administrative privileges.
1 parent f5a539c commit 4cad5d5

File tree

3 files changed

+270
-122
lines changed

3 files changed

+270
-122
lines changed

src/build.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
'core.widgets.yasb.whkd',
2727
'core.widgets.yasb.taskbar',
2828
'core.widgets.komorebi.active_layout',
29-
'core.widgets.komorebi.workspaces',
29+
'core.widgets.komorebi.workspaces'
3030
],
3131

3232
"silent_level": 1,
@@ -84,7 +84,7 @@
8484
target_name="yasb.exe",
8585
),
8686
Executable(
87-
"core/utils/yasb_cli.py",
87+
"core/utils/cli.py",
8888
base="Console",
8989
copyright=f"Copyright (C) {datetime.datetime.now().year} AmN",
9090
target_name="yasbc.exe",

src/core/utils/cli.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import argparse
2+
import subprocess
3+
import sys
4+
import logging
5+
import os
6+
import time
7+
import re
8+
import winshell
9+
import psutil
10+
import win32com.client
11+
import datetime
12+
import getpass
13+
import win32security
14+
import ctypes
15+
from settings import BUILD_VERSION
16+
from colorama import just_fix_windows_console
17+
just_fix_windows_console()
18+
19+
# Check if exe file exists
20+
OS_STARTUP_FOLDER = os.path.join(os.environ['APPDATA'], r'Microsoft\Windows\Start Menu\Programs\Startup')
21+
INSTALLATION_PATH = os.path.abspath(os.path.join(__file__, "../../.."))
22+
EXE_PATH = os.path.join(INSTALLATION_PATH, 'yasb.exe')
23+
SHORTCUT_FILENAME = "yasb.lnk"
24+
AUTOSTART_FILE = EXE_PATH if os.path.exists(EXE_PATH) else None
25+
WORKING_DIRECTORY = INSTALLATION_PATH if os.path.exists(EXE_PATH) else None
26+
27+
def is_process_running(process_name):
28+
for proc in psutil.process_iter(['name']):
29+
if proc.info['name'] == process_name:
30+
return True
31+
return False
32+
33+
class Format:
34+
end = '\033[0m'
35+
underline = '\033[4m'
36+
gray = '\033[90m'
37+
red = '\033[91m'
38+
yellow = '\033[93m'
39+
blue = '\033[94m'
40+
green = '\033[92m'
41+
42+
def format_log_line(line):
43+
line = re.sub(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}', f"{Format.gray}\\g<0>{Format.end}", line, count=1)
44+
45+
log_levels = ["CRITICAL", "ERROR", "WARNING", "NOTICE", "INFO", "DEBUG", "TRACE"]
46+
for level in log_levels:
47+
if level in line:
48+
padded_level = level.ljust(8)
49+
if level in ['CRITICAL', 'ERROR']:
50+
line = line.replace(level, f"{Format.red}{padded_level}{Format.end}")
51+
elif level == "WARNING":
52+
line = line.replace(level, f"{Format.yellow}{padded_level}{Format.end}")
53+
elif level == "NOTICE":
54+
line = line.replace(level, f"{Format.green}{padded_level}{Format.end}")
55+
elif level == "TRACE":
56+
line = line.replace(level, f"{Format.blue}{padded_level}{Format.end}")
57+
else:
58+
line = line.replace(level, padded_level)
59+
break
60+
return line
61+
62+
class CustomArgumentParser(argparse.ArgumentParser):
63+
def error(self, message):
64+
print(f'\n{Format.red}Error:{Format.end} {message}\n')
65+
sys.exit(2)
66+
67+
class CLIHandler:
68+
69+
def _enable_startup():
70+
shortcut_path = os.path.join(OS_STARTUP_FOLDER, SHORTCUT_FILENAME)
71+
try:
72+
with winshell.shortcut(shortcut_path) as shortcut:
73+
shortcut.path = AUTOSTART_FILE
74+
shortcut.working_directory = WORKING_DIRECTORY
75+
shortcut.description = "Shortcut to yasb.exe"
76+
logging.info(f"Created shortcut at {shortcut_path}")
77+
print(f"Created shortcut at {shortcut_path}")
78+
except Exception as e:
79+
logging.error(f"Failed to create startup shortcut: {e}")
80+
print(f"Failed to create startup shortcut: {e}")
81+
82+
def _disable_startup():
83+
shortcut_path = os.path.join(OS_STARTUP_FOLDER, SHORTCUT_FILENAME)
84+
if os.path.exists(shortcut_path):
85+
try:
86+
os.remove(shortcut_path)
87+
logging.info(f"Removed shortcut from {shortcut_path}")
88+
print(f"Removed shortcut from {shortcut_path}")
89+
except Exception as e:
90+
logging.error(f"Failed to remove startup shortcut: {e}")
91+
print(f"Failed to remove startup shortcut: {e}")
92+
93+
94+
def parse_arguments():
95+
parser = CustomArgumentParser(description="The command-line interface for YASB Reborn.", add_help=False)
96+
subparsers = parser.add_subparsers(dest='command', help='Commands')
97+
98+
subparsers.add_parser('start', help='Start the application')
99+
subparsers.add_parser('stop', help='Stop the application')
100+
subparsers.add_parser('reload', help='Reload the application')
101+
102+
enable_autostart_parser = subparsers.add_parser('enable-autostart', help='Enable autostart on system boot')
103+
enable_autostart_parser.add_argument('--task', action='store_true', help='Enable autostart as a scheduled task')
104+
105+
disable_autostart_parser = subparsers.add_parser('disable-autostart', help='Disable autostart on system boot')
106+
disable_autostart_parser.add_argument('--task', action='store_true', help='Disable autostart as a scheduled task')
107+
108+
subparsers.add_parser('help', help='Show help message')
109+
subparsers.add_parser('log', help='Tail yasb process logs (cancel with Ctrl-C)')
110+
parser.add_argument('-v', '--version', action='version', version=f'YASB Reborn v{BUILD_VERSION}', help="Show program's version number and exit.")
111+
parser.add_argument('-h', '--help', action='store_true', help='Show help message')
112+
args = parser.parse_args()
113+
114+
if args.command == 'start':
115+
print("Start YASB in background...")
116+
subprocess.Popen(["yasb.exe"])
117+
sys.exit(0)
118+
119+
elif args.command == 'stop':
120+
print("Stop YASB...")
121+
subprocess.run(["taskkill", "/f", "/im", "yasb.exe"], creationflags=subprocess.CREATE_NO_WINDOW)
122+
sys.exit(0)
123+
124+
elif args.command == 'reload':
125+
if is_process_running("yasb.exe"):
126+
print("Reload YASB...")
127+
subprocess.run(["taskkill", "/f", "/im", "yasb.exe"], creationflags=subprocess.CREATE_NO_WINDOW)
128+
subprocess.Popen(["yasb.exe"])
129+
else:
130+
print("YASB is not running. Reload aborted.")
131+
sys.exit(0)
132+
133+
elif args.command == 'enable-autostart':
134+
if args.task:
135+
if not CLITaskHandler.is_admin():
136+
print("Please run this command as an administrator.")
137+
else:
138+
CLITaskHandler.create_task()
139+
else:
140+
CLIHandler._enable_startup()
141+
sys.exit(0)
142+
143+
elif args.command == 'disable-autostart':
144+
if args.task:
145+
if not CLITaskHandler.is_admin():
146+
print("Please run this command as an administrator.")
147+
else:
148+
CLITaskHandler.delete_task()
149+
else:
150+
CLIHandler._disable_startup()
151+
sys.exit(0)
152+
153+
elif args.command == 'log':
154+
log_file = os.path.join(os.path.expanduser("~"), ".config", "yasb", "yasb.log")
155+
try:
156+
with open(log_file, 'r') as f:
157+
f.seek(0, os.SEEK_END)
158+
while True:
159+
line = f.readline()
160+
if not line:
161+
time.sleep(0.1)
162+
continue
163+
formatted_line = format_log_line(line)
164+
print(formatted_line, end='')
165+
except KeyboardInterrupt:
166+
pass
167+
sys.exit(0)
168+
169+
elif args.command == 'help' or args.help:
170+
print("The command-line interface for YASB Reborn.")
171+
print('\n' + Format.underline + 'Usage' + Format.end + ': yasbc <COMMAND>')
172+
print('\n' + Format.underline + 'Commands' + Format.end + ':')
173+
print(" start Start the application")
174+
print(" stop Stop the application")
175+
print(" reload Reload the application")
176+
print(" enable-autostart Enable autostart on system boot")
177+
print(" disable-autostart Disable autostart on system boot")
178+
print(" log Tail yasb process logs (cancel with Ctrl-C)")
179+
print(" help Print this message")
180+
print('\n' + Format.underline + 'Options' + Format.end + ':')
181+
print(" --version Print version")
182+
print(" --help Print this message")
183+
sys.exit(0)
184+
185+
else:
186+
logging.info("Unknown command. Use --help for available options.")
187+
sys.exit(1)
188+
189+
190+
191+
class CLITaskHandler:
192+
193+
def is_admin():
194+
try:
195+
return ctypes.windll.shell32.IsUserAnAdmin()
196+
except:
197+
return False
198+
199+
def get_current_user_sid():
200+
username = getpass.getuser()
201+
domain = os.environ['USERDOMAIN']
202+
user, domain, type = win32security.LookupAccountName(None, username)
203+
sid = win32security.ConvertSidToStringSid(user)
204+
return sid
205+
206+
def create_task():
207+
scheduler = win32com.client.Dispatch('Schedule.Service')
208+
scheduler.Connect()
209+
root_folder = scheduler.GetFolder('\\')
210+
task_def = scheduler.NewTask(0)
211+
task_def.RegistrationInfo.Description = 'A highly configurable Windows status bar.'
212+
task_def.RegistrationInfo.Author = "AmN"
213+
task_def.Settings.Compatibility = 6
214+
trigger = task_def.Triggers.Create(9)
215+
trigger.Enabled = True
216+
trigger.StartBoundary = datetime.datetime.now().isoformat()
217+
principal = task_def.Principal
218+
principal.UserId = CLITaskHandler.get_current_user_sid()
219+
principal.LogonType = 3
220+
principal.RunLevel = 0
221+
settings = task_def.Settings
222+
settings.Enabled = True
223+
settings.StartWhenAvailable = True
224+
settings.AllowHardTerminate = True
225+
settings.ExecutionTimeLimit = 'PT0S'
226+
settings.Priority = 7
227+
settings.MultipleInstances = 0
228+
settings.DisallowStartIfOnBatteries = False
229+
settings.StopIfGoingOnBatteries = False
230+
settings.Hidden = False
231+
settings.RunOnlyIfIdle = False
232+
settings.DisallowStartOnRemoteAppSession = False
233+
settings.UseUnifiedSchedulingEngine = True
234+
settings.WakeToRun = False
235+
idle_settings = settings.IdleSettings
236+
idle_settings.StopOnIdleEnd = True
237+
idle_settings.RestartOnIdle = False
238+
action = task_def.Actions.Create(0)
239+
action.Path = EXE_PATH
240+
action.WorkingDirectory = INSTALLATION_PATH
241+
try:
242+
root_folder.RegisterTaskDefinition(
243+
'YASB Reborn',
244+
task_def,
245+
6,
246+
None,
247+
None,
248+
3,
249+
None
250+
)
251+
print(f"Task YASB Reborn created successfully.")
252+
except Exception as e:
253+
print(f"Failed to create task YASB Reborn. Error: {e}")
254+
255+
def delete_task():
256+
scheduler = win32com.client.Dispatch('Schedule.Service')
257+
scheduler.Connect()
258+
root_folder = scheduler.GetFolder('\\')
259+
try:
260+
root_folder.DeleteTask('YASB Reborn', 0)
261+
print(f"Task YASB Reborn deleted successfully.")
262+
except Exception:
263+
print(f"Failed to delete task YASB or task does not exist.")
264+
265+
266+
if __name__ == "__main__":
267+
CLIHandler.parse_arguments()
268+
sys.exit(0)

0 commit comments

Comments
 (0)