Skip to content

Instantly share code, notes, and snippets.

@roflsunriz
Created February 5, 2025 13:03
Show Gist options
  • Save roflsunriz/a819e7ed37854f4346e118120e64be45 to your computer and use it in GitHub Desktop.
Save roflsunriz/a819e7ed37854f4346e118120e64be45 to your computer and use it in GitHub Desktop.
auto_cursor.pyw : Cursorを常駐させておくGUIプログラム
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