Created
February 5, 2025 13:03
-
-
Save roflsunriz/a819e7ed37854f4346e118120e64be45 to your computer and use it in GitHub Desktop.
auto_cursor.pyw : Cursorを常駐させておくGUIプログラム
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import time | |
import psutil | |
import win32gui | |
import win32con | |
from subprocess import Popen | |
import tkinter as tk | |
from tkinter import ttk, messagebox | |
import winreg | |
import sys | |
import pystray | |
from PIL import Image | |
import datetime | |
import subprocess | |
import argparse | |
def is_cursor_running(): | |
return "Cursor.exe" in (p.name() for p in psutil.process_iter()) | |
def minimize_cursor_window(): | |
def callback(hwnd, _): | |
if win32gui.IsWindowVisible(hwnd) and "Cursor" in win32gui.GetWindowText(hwnd): | |
win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) | |
win32gui.EnumWindows(callback, None) | |
def get_cursor_path(): | |
search_paths = [ | |
os.path.join(os.environ.get('LOCALAPPDATA'), 'Programs', 'Cursor', 'Cursor.exe'), | |
os.path.expandvars('%ProgramFiles%\\Cursor\\Cursor.exe'), | |
os.path.expanduser('~\\AppData\\Local\\Programs\\Cursor\\Cursor.exe') | |
] | |
for path in search_paths: | |
if os.path.exists(path): | |
return path | |
raise FileNotFoundError("Cursorの実行ファイルが見つかりませんでした") | |
class AppTray: | |
def __init__(self): | |
self.root = tk.Tk() | |
self.root.title("Cursor Guardian") | |
self.interval_value = None | |
self.log_text = None | |
self.log_messages = [] | |
self.resource_stats = [] | |
self.setup_ui() | |
self.running = True | |
self.check_interval = 60 # 秒 | |
self.tray_icon = None | |
self.setup_tray_icon() | |
self.setup_autostart() | |
def setup_ui(self): | |
# ステータス表示 | |
self.status_label = ttk.Label(self.root, text="Cursor Status: ") | |
self.status_indicator = tk.Canvas(self.root, width=20, height=20) | |
# ステータス表示改良 | |
self.status_text = ttk.Label(self.root, text="Unknown", font=('Arial', 10, 'bold')) | |
self.status_text.grid(row=0, column=2, padx=10, sticky='w') | |
# 操作ボタン | |
self.force_kill_btn = ttk.Button(self.root, text="強制終了", command=self.force_stop) | |
self.restart_btn = ttk.Button(self.root, text="再起動", command=self.restart_cursor) | |
self.exit_btn = ttk.Button(self.root, text="スクリプト終了", command=self.safe_exit) | |
# 設定項目 | |
self.interval_scale = ttk.Scale(self.root, from_=10, to=300, | |
orient='horizontal', | |
command=self.update_interval_display) | |
self.interval_scale.set(60) | |
# チェック間隔表示追加 | |
self.interval_value = ttk.Label(self.root, text="60 秒") | |
self.interval_value.grid(row=4, column=1, padx=10, sticky='e') | |
# 自動起動チェックボックス | |
self.autostart_var = tk.BooleanVar(value=self.check_autostart()) | |
self.autostart_check = ttk.Checkbutton( | |
self.root, | |
text="Windows起動時に自動開始", | |
variable=self.autostart_var, | |
command=self.toggle_autostart | |
) | |
# グリッド配置 | |
self.status_label.grid(row=0, column=0, padx=10, pady=10, sticky='w') | |
self.status_indicator.grid(row=0, column=1, padx=10, pady=10, sticky='e') | |
self.force_kill_btn.grid(row=1, column=0, padx=5, pady=2, sticky='ew') | |
self.restart_btn.grid(row=1, column=1, padx=5, pady=2, sticky='ew') | |
self.exit_btn.grid(row=2, column=0, columnspan=2, padx=5, pady=5, sticky='ew') | |
self.autostart_check.grid(row=3, column=0, columnspan=2, pady=5, sticky='w') | |
# チェック間隔設定フレーム | |
interval_frame = ttk.LabelFrame(self.root, text="チェック間隔設定(秒)") | |
interval_frame.grid(row=4, column=0, columnspan=2, padx=10, pady=5, sticky='ew') | |
# チェック間隔スライダー更新 | |
self.interval_scale = ttk.Scale( | |
interval_frame, | |
from_=10, | |
to=300, | |
orient='horizontal', | |
command=self.update_interval_display | |
) | |
self.interval_scale.set(60) | |
self.interval_scale.pack(fill='x', padx=5, pady=2) | |
# チェック間隔表示ラベルの正式な初期化 | |
self.interval_value = ttk.Label(interval_frame, text="60 秒") | |
self.interval_value.pack(pady=2) | |
# バージョン表示 | |
version_label = ttk.Label(self.root, text="Cursor Guardian v1.0", foreground="gray") | |
version_label.grid(row=5, column=0, columnspan=2, pady=10) | |
# リソース表示フレーム | |
resource_frame = ttk.LabelFrame(self.root, text="リソース使用状況") | |
resource_frame.grid(row=6, column=0, columnspan=2, padx=10, pady=5, sticky='ew') | |
self.cpu_label = ttk.Label(resource_frame, text="CPU: 0%") | |
self.mem_label = ttk.Label(resource_frame, text="メモリ: 0MB") | |
self.cpu_label.pack(side='left', padx=10) | |
self.mem_label.pack(side='left', padx=10) | |
# ログ表示エリア | |
log_frame = ttk.LabelFrame(self.root, text="操作ログ") | |
log_frame.grid(row=7, column=0, columnspan=2, padx=10, pady=5, sticky='nsew') | |
self.log_text = tk.Text(log_frame, height=5, state='disabled') | |
scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview) | |
self.log_text.configure(yscrollcommand=scrollbar.set) | |
self.log_text.pack(side='left', fill='both', expand=True) | |
scrollbar.pack(side='right', fill='y') | |
# グリッドの行設定 | |
self.root.rowconfigure(7, weight=1) | |
def update_status(self, is_running): | |
color = "green" if is_running else "red" | |
status = "Running" if is_running else "Terminated" | |
self.status_indicator.delete("all") | |
self.status_indicator.create_oval(2, 2, 18, 18, fill=color) | |
self.status_text.config(text=status, foreground=color) | |
def update_resources(self): | |
cpu_percent = psutil.cpu_percent() | |
mem_info = psutil.virtual_memory() | |
self.cpu_label.config(text=f"CPU: {cpu_percent}%") | |
self.mem_label.config(text=f"メモリ: {mem_info.used//1024//1024}MB") | |
self.resource_stats.append((datetime.datetime.now(), cpu_percent, mem_info.used)) | |
# 過去60件保持 | |
if len(self.resource_stats) > 60: | |
self.resource_stats.pop(0) | |
def add_log(self, message): | |
if not hasattr(self, 'log_text') or self.log_text is None: | |
return | |
timestamp = datetime.datetime.now().strftime("%H:%M:%S") | |
self.log_messages.append(f"[{timestamp}] {message}") | |
self.log_text.configure(state='normal') | |
self.log_text.insert('end', f"[{timestamp}] {message}\n") | |
self.log_text.see('end') | |
self.log_text.configure(state='disabled') | |
# ログを100行まで保持 | |
if len(self.log_messages) > 100: | |
self.log_messages.pop(0) | |
def force_stop(self): | |
if messagebox.askyesno("確認", "本当にCursorを終了しますか?"): | |
for proc in psutil.process_iter(): | |
if proc.name() == "Cursor.exe": | |
proc.kill() | |
self.update_status(False) | |
self.add_log("Cursorを強制終了しました") | |
def safe_exit(self): | |
self.running = False | |
self.root.destroy() | |
def restart_cursor(self): | |
if not is_cursor_running(): | |
Popen(get_cursor_path()) | |
minimize_cursor_window() | |
self.add_log("Cursorを再起動しました") | |
def setup_tray_icon(self): | |
menu = pystray.Menu( | |
pystray.MenuItem('開く', self.restore_window), | |
pystray.MenuItem('終了', self.safe_exit) | |
) | |
image = Image.new('RGB', (64, 64), 'white') | |
self.tray_icon = pystray.Icon("cursor_guardian", image, menu=menu) | |
def toggle_autostart(self): | |
# パス取得方法修正 | |
exe_path = sys.executable | |
script_path = os.path.abspath(sys.argv[0]) | |
# パス存在チェック追加 | |
if not os.path.exists(script_path): | |
messagebox.showerror("エラー", f"スクリプトパスが見つかりません:\n{script_path}") | |
return | |
key_path = r"Software\Microsoft\Windows\CurrentVersion\Run" | |
app_name = "CursorGuardian" | |
try: | |
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_WRITE) as key: | |
if self.autostart_var.get(): | |
winreg.SetValueEx(key, app_name, 0, winreg.REG_SZ, f'"{exe_path}" "{script_path}"') | |
else: | |
winreg.DeleteValue(key, app_name) | |
except Exception as e: | |
messagebox.showerror("エラー", f"自動起動設定に失敗しました\n{str(e)}") | |
def check_autostart(self): | |
try: | |
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Run") as key: | |
value, _ = winreg.QueryValueEx(key, "CursorGuardian") | |
return bool(value) | |
except FileNotFoundError: | |
return False | |
except Exception: | |
return False | |
def minimize_to_tray(self): | |
self.root.withdraw() | |
self.tray_icon.run_detached() | |
def restore_window(self): | |
self.root.deiconify() | |
self.tray_icon.stop() | |
def main_loop(self): | |
# パフォーマンス最適化 | |
self.root.after(100, self._periodic_check) | |
self.root.mainloop() | |
def _periodic_check(self): | |
if self.running: | |
self.update_resources() | |
self.check_cursor_status() | |
# チェック間隔を動的に適用 | |
self.root.after(int(self.check_interval * 1000), self._periodic_check) | |
def check_cursor_status(self): | |
# 既存のステータスチェック処理 | |
is_running = is_cursor_running() | |
self.update_status(is_running) | |
if not is_running: | |
self.add_log("Cursorプロセス消失を検知、再起動します") | |
self.restart_cursor() | |
def setup_autostart(self): | |
"""パフォーマンス改善版""" | |
try: | |
current_state = self.check_autostart() | |
self.autostart_var.set(current_state) | |
# 初回のみレジストリ更新 | |
if current_state != self.check_autostart(): | |
self.toggle_autostart() | |
except Exception as e: | |
messagebox.showerror("エラー", f"初期化失敗: {str(e)}") | |
def update_interval_display(self, value): | |
"""スライダーの値変更時に呼ばれる""" | |
if self.interval_value is not None: # 安全策追加 | |
seconds = round(float(value)) | |
self.check_interval = seconds | |
self.interval_value.config(text=f"{seconds} 秒") | |
self.add_log(f"チェック間隔を{seconds}秒に設定") | |
# 自動インストール設定 | |
def auto_install_dependencies(skip=False): | |
required = { | |
'psutil': 'psutil', | |
'win32gui': 'pywin32', # モジュール名とパッケージ名を明示 | |
'pystray': 'pystray', | |
'PIL': 'Pillow' | |
} | |
missing = [] | |
for package, install_name in required.items(): | |
try: | |
__import__(package.split('.')[0]) # トップレベルモジュールチェック | |
except ImportError: | |
missing.append(install_name) | |
if missing and not skip: | |
print(f"不足モジュールをインストールします: {', '.join(missing)}") | |
try: | |
# pywin32用の特別処理 | |
if 'pywin32' in missing: | |
subprocess.check_call([sys.executable, "-m", "pip", "install", "pywin32"]) | |
missing.remove('pywin32') | |
if missing: | |
subprocess.check_call([sys.executable, "-m", "pip", "install", *missing]) | |
print("インストール成功!続行します") | |
# 再インポート処理 | |
for package in required: | |
try: | |
globals()[package] = __import__(package) | |
except Exception as e: | |
print(f"モジュール再読み込みエラー: {str(e)}") | |
return | |
except Exception as e: | |
print(f"インストール失敗: {str(e)}\n手動インストールを試してください:") | |
print(f"python -m pip install {' '.join(missing)}") | |
sys.exit(1) | |
elif missing and skip: | |
print("不足モジュールがあります: " + ", ".join(missing)) | |
sys.exit(1) | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser() | |
parser.add_argument('--skip-install', action='store_true', | |
help='モジュール自動インストールをスキップ') | |
args = parser.parse_args() | |
auto_install_dependencies(skip=args.skip_install) | |
# メインプロセス起動 | |
app = AppTray() | |
app.root.protocol('WM_DELETE_WINDOW', app.minimize_to_tray) | |
app.main_loop() | |
app.tray_icon.stop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment