import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, simpledialog
import threading
import time
import pyautogui
import pyperclip
from datetime import datetime
from queue import Queue
import subprocess
import json
try:
from Quartz import CGWindowListCopyWindowInfo, kCGWindowListOptionOnScreenOnly, kCGNullWindowID
from Quartz import CGEventCreateKeyboardEvent, CGEventPost, kCGHIDEventTap
from Quartz import kCGEventKeyDown, kCGEventKeyUp
from AppKit import NSWorkspace, NSApplication, NSRunningApplication
QUARTZ_AVAILABLE = True
except ImportError:
QUARTZ_AVAILABLE = False
class WindowManager:
@staticmethod
def get_applications():
"""実行中のアプリケーション一覧を取得"""
try:
if QUARTZ_AVAILABLE:
workspace = NSWorkspace.sharedWorkspace()
apps = workspace.runningApplications()
app_list = []
for app in apps:
if app.activationPolicy() == 0:
name = app.localizedName()
pid = app.processIdentifier()
if name and name != "":
app_list.append((pid, name))
return app_list
else:
script = '''
tell application "System Events"
set appList to {}
repeat with proc in (every application process whose visible is true)
set end of appList to name of proc
end repeat
return appList
end tell
'''
result = subprocess.run(['osascript', '-e', script],
capture_output=True, text=True)
if result.returncode == 0:
apps = result.stdout.strip().split(', ')
return [(i, app) for i, app in enumerate(apps) if app]
return []
except Exception as e:
print(f"アプリケーション取得エラー: {e}")
return []
@staticmethod
def get_windows():
"""全てのウィンドウを取得"""
try:
if QUARTZ_AVAILABLE:
windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID)
window_list = []
for window in windows:
if 'kCGWindowName' in window and window['kCGWindowName']:
window_id = window['kCGWindowNumber']
window_name = window['kCGWindowName']
app_name = window.get('kCGWindowOwnerName', 'Unknown')
window_list.append((window_id, f"{app_name} - {window_name}"))
return window_list
else:
apps = WindowManager.get_applications()
return [(pid, name) for pid, name in apps]
except Exception as e:
print(f"ウィンドウ取得エラー: {e}")
return []
@staticmethod
def focus_application(app_name):
"""アプリケーションにフォーカス"""
try:
script = f'tell application "{app_name}" to activate'
result = subprocess.run(['osascript', '-e', script],
capture_output=True, text=True)
return result.returncode == 0
except Exception as e:
print(f"フォーカスエラー: {e}")
return False
@staticmethod
def send_text_applescript(app_name, text):
"""AppleScriptでテキストを送信"""
try:
activate_script = f'tell application "{app_name}" to activate'
result1 = subprocess.run(['osascript', '-e', activate_script],
capture_output=True, text=True)
if result1.returncode != 0:
return False
time.sleep(0.7)
pyperclip.copy(text)
time.sleep(0.2)
paste_script = '''
tell application "System Events"
key code 9 using command down
delay 0.3
key code 36 using command down
end tell
'''
result2 = subprocess.run(['osascript', '-e', paste_script],
capture_output=True, text=True)
return result2.returncode == 0
except Exception as e:
print(f"AppleScript送信エラー: {e}")
return False
@staticmethod
def get_frontmost_app():
"""最前面のアプリケーションを取得"""
try:
if QUARTZ_AVAILABLE:
workspace = NSWorkspace.sharedWorkspace()
app = workspace.frontmostApplication()
return app.localizedName() if app else None
else:
script = '''
tell application "System Events"
set frontApp to name of first application process whose frontmost is true
return frontApp
end tell
'''
result = subprocess.run(['osascript', '-e', script],
capture_output=True, text=True)
if result.returncode == 0:
return result.stdout.strip()
return None
except Exception as e:
print(f"最前面アプリ取得エラー: {e}")
return None
class AutoChatSender:
def __init__(self, root):
self.root = root
self.root.title("AIチャット自動送信ツール(macOS版)")
self.root.geometry("800x800")
self.is_running = False
self.thread = None
self.queue_items = []
self.sent_history = []
self.selected_app_name = None
self.target_app_name = None
self.setup_ui()
pyautogui.FAILSAFE = True
pyautogui.PAUSE = 0.3
def setup_ui(self):
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# 上部設定エリア(左右に分割)
top_frame = ttk.Frame(main_frame)
top_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
# 左側:アプリケーション選択エリア
app_frame = ttk.LabelFrame(top_frame, text="送信先設定", padding="10")
app_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
ttk.Button(app_frame, text="アプリ一覧を更新", command=self.refresh_apps).grid(row=0, column=0, sticky=tk.W)
self.app_var = tk.StringVar()
self.app_combo = ttk.Combobox(app_frame, textvariable=self.app_var, width=35, state="readonly")
self.app_combo.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(5, 0))
self.app_combo.bind('<<ComboboxSelected>>', self.on_app_selected)
ttk.Button(app_frame, text="テストフォーカス", command=self.test_focus).grid(row=1, column=1, padx=(5, 0))
# 送信先指定
target_frame = ttk.Frame(app_frame)
target_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0))
self.target_button = ttk.Button(target_frame, text="送信先指定", command=self.set_target_from_frontmost)
self.target_button.pack(side=tk.LEFT)
self.target_info_var = tk.StringVar(value="送信先: 未設定")
self.target_label = tk.Label(target_frame, textvariable=self.target_info_var,
fg="white" if self.is_dark_mode() else "green",
bg=self.root.cget('bg'))
self.target_label.pack(side=tk.LEFT, padx=(10, 0))
app_frame.columnconfigure(0, weight=1)
# 右側:送信設定とステータス
right_frame = ttk.LabelFrame(top_frame, text="送信設定・ステータス", padding="10")
right_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
# 送信間隔設定
interval_frame = ttk.Frame(right_frame)
interval_frame.grid(row=0, column=0, sticky=tk.W)
ttk.Label(interval_frame, text="デフォルト間隔:").pack(side=tk.LEFT)
self.interval_var = tk.StringVar(value="10")
ttk.Entry(interval_frame, textvariable=self.interval_var, width=8).pack(side=tk.LEFT, padx=(5, 5))
ttk.Label(interval_frame, text="秒").pack(side=tk.LEFT)
# 送信方法設定
method_frame = ttk.Frame(right_frame)
method_frame.grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
ttk.Label(method_frame, text="送信方法:").pack(side=tk.LEFT)
self.method_var = tk.StringVar(value="applescript")
ttk.Radiobutton(method_frame, text="AppleScript", variable=self.method_var, value="applescript").pack(side=tk.LEFT, padx=(10, 5))
ttk.Radiobutton(method_frame, text="クリップボード", variable=self.method_var, value="clipboard").pack(side=tk.LEFT, padx=(0, 5))
ttk.Radiobutton(method_frame, text="pyautogui", variable=self.method_var, value="pyautogui").pack(side=tk.LEFT)
# ステータス表示
status_frame = ttk.Frame(right_frame)
status_frame.grid(row=2, column=0, sticky=tk.W, pady=(10, 0))
ttk.Label(status_frame, text="ステータス:").pack(side=tk.LEFT)
self.status_var = tk.StringVar(value="待機中")
self.status_label = tk.Label(status_frame, textvariable=self.status_var,
fg="white" if self.is_dark_mode() else "blue",
bg=self.root.cget('bg'))
self.status_label.pack(side=tk.LEFT, padx=(10, 0))
# 制御ボタン
control_frame = ttk.Frame(right_frame)
control_frame.grid(row=3, column=0, sticky=tk.W, pady=(10, 0))
self.start_button = ttk.Button(control_frame, text="キュー送信開始", command=self.start_queue_sending)
self.start_button.pack(side=tk.LEFT, padx=(0, 5))
self.stop_button = ttk.Button(control_frame, text="停止", command=self.stop_sending, state=tk.DISABLED)
self.stop_button.pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(control_frame, text="テスト送信", command=self.test_send).pack(side=tk.LEFT)
# 上部フレームの列重み設定
top_frame.columnconfigure(0, weight=1)
top_frame.columnconfigure(1, weight=1)
# 文章入力エリア
input_frame = ttk.LabelFrame(main_frame, text="文章入力(Cmd+Return でキューに追加)", padding="10")
input_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
self.text_area = scrolledtext.ScrolledText(input_frame, wrap=tk.WORD, width=70, height=8, font=("Arial", 12))
self.text_area.grid(row=0, column=0, columnspan=4, sticky=(tk.W, tk.E, tk.N, tk.S), padx=5, pady=5)
self.text_area.bind('<Command-Return>', self.add_to_queue_shortcut)
# 待機時間設定
wait_frame = ttk.Frame(input_frame)
wait_frame.grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
ttk.Label(wait_frame, text="この項目の待機時間:").pack(side=tk.LEFT)
self.item_wait_var = tk.StringVar(value="10")
ttk.Entry(wait_frame, textvariable=self.item_wait_var, width=8).pack(side=tk.LEFT, padx=(5, 5))
ttk.Label(wait_frame, text="秒").pack(side=tk.LEFT)
# ボタンフレーム
button_frame = ttk.Frame(input_frame)
button_frame.grid(row=2, column=0, sticky=tk.W, pady=(10, 0))
ttk.Button(button_frame, text="キューに追加", command=self.add_to_queue).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(button_frame, text="クリア", command=self.clear_input).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(button_frame, text="キューをクリア", command=self.clear_queue).pack(side=tk.LEFT)
input_frame.columnconfigure(0, weight=1)
input_frame.rowconfigure(0, weight=1)
# キュー表示エリア
queue_frame = ttk.LabelFrame(main_frame, text="送信キュー", padding="10")
queue_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
columns = ('text', 'wait_time', 'status')
self.queue_tree = ttk.Treeview(queue_frame, columns=columns, show='headings', height=8)
self.queue_tree.heading('text', text='送信内容')
self.queue_tree.heading('wait_time', text='待機時間')
self.queue_tree.heading('status', text='状態')
self.queue_tree.column('text', width=350)
self.queue_tree.column('wait_time', width=80)
self.queue_tree.column('status', width=80)
self.queue_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
queue_scrollbar = ttk.Scrollbar(queue_frame, orient="vertical", command=self.queue_tree.yview)
queue_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
self.queue_tree.config(yscrollcommand=queue_scrollbar.set)
# キュー操作ボタン
queue_button_frame = ttk.Frame(queue_frame)
queue_button_frame.grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
ttk.Button(queue_button_frame, text="選択項目を削除", command=self.remove_selected_from_queue).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(queue_button_frame, text="上に移動", command=self.move_up).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(queue_button_frame, text="下に移動", command=self.move_down).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(queue_button_frame, text="待機時間編集", command=self.edit_wait_time).pack(side=tk.LEFT, padx=(5, 0))
queue_frame.columnconfigure(0, weight=1)
queue_frame.rowconfigure(0, weight=1)
# 送信履歴エリア
history_frame = ttk.LabelFrame(main_frame, text="送信履歴", padding="10")
history_frame.grid(row=3, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
history_columns = ('time', 'text', 'wait_time')
self.history_tree = ttk.Treeview(history_frame, columns=history_columns, show='headings', height=6)
self.history_tree.heading('time', text='送信時刻')
self.history_tree.heading('text', text='送信内容')
self.history_tree.heading('wait_time', text='待機時間')
self.history_tree.column('time', width=120)
self.history_tree.column('text', width=300)
self.history_tree.column('wait_time', width=80)
self.history_tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
history_scrollbar = ttk.Scrollbar(history_frame, orient="vertical", command=self.history_tree.yview)
history_scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
self.history_tree.config(yscrollcommand=history_scrollbar.set)
# 履歴操作ボタン
history_button_frame = ttk.Frame(history_frame)
history_button_frame.grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
ttk.Button(history_button_frame, text="選択項目を再送信", command=self.resend_from_history).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(history_button_frame, text="履歴をクリア", command=self.clear_history).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(history_button_frame, text="全履歴を再キュー", command=self.requeue_all_history).pack(side=tk.LEFT)
history_frame.columnconfigure(0, weight=1)
history_frame.rowconfigure(0, weight=1)
# ログエリア
log_frame = ttk.LabelFrame(main_frame, text="ログ", padding="10")
log_frame.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(10, 0))
self.log_area = scrolledtext.ScrolledText(log_frame, wrap=tk.WORD, width=70, height=4)
self.log_area.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
log_frame.columnconfigure(0, weight=1)
log_frame.rowconfigure(0, weight=1)
# グリッド設定
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
main_frame.rowconfigure(1, weight=1)
main_frame.rowconfigure(2, weight=2)
main_frame.rowconfigure(3, weight=2)
main_frame.rowconfigure(4, weight=1)
self.refresh_apps()
def is_dark_mode(self):
"""ダークモード判定"""
try:
# macOSのダークモード判定
script = '''
tell application "System Events"
tell appearance preferences
return dark mode
end tell
end tell
'''
result = subprocess.run(['osascript', '-e', script],
capture_output=True, text=True)
return result.returncode == 0 and 'true' in result.stdout.lower()
except:
# Tkinterの背景色で判定(フォールバック)
bg_color = self.root.cget('bg')
return bg_color in ['#3c3c3c', '#2d2d2d', '#1e1e1e', 'systemWindowBackgroundColor']
def update_status(self, status, color_type="normal"):
"""ステータスを更新(ダークモード対応)"""
self.status_var.set(status)
if color_type == "success":
color = "#00ff00" if self.is_dark_mode() else "green"
elif color_type == "error":
color = "#ff6666" if self.is_dark_mode() else "red"
elif color_type == "warning":
color = "#ffaa00" if self.is_dark_mode() else "orange"
else: # normal
color = "#66ccff" if self.is_dark_mode() else "blue"
self.status_label.config(fg=color)
def update_target_info(self, info, color_type="normal"):
"""送信先情報を更新(ダークモード対応)"""
self.target_info_var.set(info)
if color_type == "success":
color = "#00ff00" if self.is_dark_mode() else "green"
elif color_type == "error":
color = "#ff6666" if self.is_dark_mode() else "red"
else: # normal
color = "#66ccff" if self.is_dark_mode() else "blue"
self.target_label.config(fg=color)
def refresh_apps(self):
"""アプリケーション一覧を更新"""
try:
apps = WindowManager.get_applications()
priority_keywords = ['chrome', 'firefox', 'safari', 'edge', 'claude', 'chatgpt', 'discord', 'slack', 'teams']
sorted_apps = []
other_apps = []
for pid, name in apps:
name_lower = name.lower()
if any(keyword in name_lower for keyword in priority_keywords):
sorted_apps.append((pid, name))
else:
other_apps.append((pid, name))
all_apps = sorted_apps + other_apps
app_list = [name for pid, name in all_apps]
self.app_combo['values'] = app_list
self.log_message(f"アプリケーション一覧を更新しました({len(app_list)}個)")
except Exception as e:
self.log_message(f"アプリケーション一覧の取得に失敗: {str(e)}")
def on_app_selected(self, event):
"""アプリが選択された時の処理"""
selection = self.app_var.get()
if selection:
self.selected_app_name = selection
self.log_message(f"送信先アプリを設定: {selection}")
def test_focus(self):
"""フォーカステスト"""
if self.selected_app_name:
if WindowManager.focus_application(self.selected_app_name):
self.log_message("フォーカステスト成功")
else:
self.log_message("フォーカステスト失敗")
else:
messagebox.showwarning("警告", "送信先アプリを選択してください")
def set_target_from_frontmost(self):
"""最前面のアプリを送信先に設定"""
self.target_button.config(state=tk.DISABLED)
self.update_target_info("3秒後に最前面アプリを取得...", "warning")
def get_target():
try:
for i in range(3, 0, -1):
self.update_target_info(f"送信先取得まで {i} 秒... AIチャットを最前面にしてください", "warning")
self.root.update()
time.sleep(1)
frontmost_app = WindowManager.get_frontmost_app()
if frontmost_app:
self.target_app_name = frontmost_app
self.update_target_info(f"送信先: {frontmost_app}", "success")
self.log_message(f"送信先設定完了: {frontmost_app}")
self.selected_app_name = frontmost_app
else:
self.update_target_info("送信先: 取得失敗", "error")
self.log_message("送信先の取得に失敗しました")
except Exception as e:
self.update_target_info("送信先: エラー", "error")
self.log_message(f"送信先設定エラー: {str(e)}")
finally:
self.target_button.config(state=tk.NORMAL)
threading.Thread(target=get_target, daemon=True).start()
def add_to_queue_shortcut(self, event):
"""Cmd+Returnでキューに追加"""
self.add_to_queue()
return 'break'
def add_to_queue(self):
"""テキストをキューに追加"""
text = self.text_area.get("1.0", tk.END).strip()
if not text:
messagebox.showwarning("警告", "送信する文章を入力してください")
return
try:
wait_time = int(self.item_wait_var.get())
if wait_time < 1:
messagebox.showwarning("警告", "待機時間は1秒以上にしてください")
return
except ValueError:
messagebox.showwarning("警告", "待機時間は数値で入力してください")
return
queue_item = {
'text': text,
'wait_time': wait_time,
'status': '待機中'
}
self.queue_items.append(queue_item)
preview = text[:40] + "..." if len(text) > 40 else text
self.queue_tree.insert('', 'end', values=(preview, f"{wait_time}秒", "待機中"))
self.log_message(f"キューに追加: {preview} (待機時間: {wait_time}秒)")
self.text_area.delete("1.0", tk.END)
def edit_wait_time(self):
"""選択されたキューアイテムの待機時間を編集"""
selection = self.queue_tree.selection()
if not selection:
messagebox.showwarning("警告", "編集する項目を選択してください")
return
item_index = self.queue_tree.index(selection[0])
current_wait_time = self.queue_items[item_index]['wait_time']
new_wait_time = tk.simpledialog.askinteger(
"待機時間編集",
f"新しい待機時間を入力してください(現在: {current_wait_time}秒):",
initialvalue=current_wait_time,
minvalue=1,
maxvalue=3600
)
if new_wait_time:
self.queue_items[item_index]['wait_time'] = new_wait_time
item_id = selection[0]
current_values = list(self.queue_tree.item(item_id, 'values'))
current_values[1] = f"{new_wait_time}秒"
self.queue_tree.item(item_id, values=current_values)
self.log_message(f"待機時間を更新: {new_wait_time}秒")
def resend_from_history(self):
"""履歴から選択されたアイテムを再送信"""
selection = self.history_tree.selection()
if not selection:
messagebox.showwarning("警告", "再送信する項目を選択してください")
return
item_index = self.history_tree.index(selection[0])
history_item = self.sent_history[item_index]
queue_item = {
'text': history_item['text'],
'wait_time': history_item['wait_time'],
'status': '待機中'
}
self.queue_items.append(queue_item)
preview = history_item['text'][:40] + "..." if len(history_item['text']) > 40 else history_item['text']
self.queue_tree.insert('', 'end', values=(preview, f"{history_item['wait_time']}秒", "待機中"))
self.log_message(f"履歴から再キュー: {preview}")
def requeue_all_history(self):
"""全ての送信履歴をキューに再追加"""
if not self.sent_history:
messagebox.showinfo("情報", "送信履歴がありません")
return
confirm = messagebox.askyesno("確認", f"{len(self.sent_history)}件の履歴をすべてキューに追加しますか?")
if not confirm:
return
for history_item in self.sent_history:
queue_item = {
'text': history_item['text'],
'wait_time': history_item['wait_time'],
'status': '待機中'
}
self.queue_items.append(queue_item)
preview = history_item['text'][:40] + "..." if len(history_item['text']) > 40 else history_item['text']
self.queue_tree.insert('', 'end', values=(preview, f"{history_item['wait_time']}秒", "待機中"))
self.log_message(f"全履歴をキューに追加: {len(self.sent_history)}件")
def clear_history(self):
"""送信履歴をクリア"""
if not self.sent_history:
return
confirm = messagebox.askyesno("確認", "送信履歴をすべて削除しますか?")
if confirm:
self.sent_history.clear()
for item in self.history_tree.get_children():
self.history_tree.delete(item)
self.log_message("送信履歴をクリアしました")
def clear_input(self):
"""入力エリアをクリア"""
self.text_area.delete("1.0", tk.END)
def clear_queue(self):
"""キューをクリア"""
self.queue_items.clear()
for item in self.queue_tree.get_children():
self.queue_tree.delete(item)
self.log_message("キューをクリアしました")
def remove_selected_from_queue(self):
"""選択された項目をキューから削除"""
selection = self.queue_tree.selection()
if not selection:
messagebox.showwarning("警告", "削除する項目を選択してください")
return
item_index = self.queue_tree.index(selection[0])
del self.queue_items[item_index]
self.queue_tree.delete(selection[0])
self.log_message(f"項目 {item_index + 1} をキューから削除しました")
def move_up(self):
"""選択項目を上に移動"""
selection = self.queue_tree.selection()
if not selection:
return
item_index = self.queue_tree.index(selection[0])
if item_index == 0:
return
self.queue_items[item_index], self.queue_items[item_index - 1] = \
self.queue_items[item_index - 1], self.queue_items[item_index]
self.refresh_queue_display()
new_items = self.queue_tree.get_children()
if item_index - 1 < len(new_items):
self.queue_tree.selection_set(new_items[item_index - 1])
def move_down(self):
"""選択項目を下に移動"""
selection = self.queue_tree.selection()
if not selection:
return
item_index = self.queue_tree.index(selection[0])
if item_index >= len(self.queue_items) - 1:
return
self.queue_items[item_index], self.queue_items[item_index + 1] = \
self.queue_items[item_index + 1], self.queue_items[item_index]
self.refresh_queue_display()
new_items = self.queue_tree.get_children()
if item_index + 1 < len(new_items):
self.queue_tree.selection_set(new_items[item_index + 1])
def refresh_queue_display(self):
"""キュー表示を更新"""
for item in self.queue_tree.get_children():
self.queue_tree.delete(item)
for queue_item in self.queue_items:
preview = queue_item['text'][:40] + "..." if len(queue_item['text']) > 40 else queue_item['text']
self.queue_tree.insert('', 'end', values=(
preview,
f"{queue_item['wait_time']}秒",
queue_item['status']
))
def log_message(self, message):
"""ログメッセージを追加"""
timestamp = datetime.now().strftime("%H:%M:%S")
log_entry = f"[{timestamp}] {message}\n"
self.log_area.insert(tk.END, log_entry)
self.log_area.see(tk.END)
self.root.update()
def send_text(self, text):
"""文章を送信"""
try:
method = self.method_var.get()
target_app = self.target_app_name if self.target_app_name else self.selected_app_name
if method == "clipboard":
try:
pyperclip.copy(text)
time.sleep(0.3)
current_clipboard = pyperclip.paste()
if current_clipboard != text:
return False
if target_app:
WindowManager.focus_application(target_app)
time.sleep(1.0)
paste_script = '''
tell application "System Events"
key code 9 using command down
delay 0.5
key code 36 using command down
end tell
'''
result = subprocess.run(['osascript', '-e', paste_script],
capture_output=True, text=True)
return result.returncode == 0
except Exception as e:
return False
elif method == "applescript":
if not target_app:
self.log_message("送信先アプリが設定されていません")
return False
return WindowManager.send_text_applescript(target_app, text)
else: # pyautogui
if target_app:
WindowManager.focus_application(target_app)
time.sleep(1.0)
pyperclip.copy(text)
time.sleep(0.3)
paste_script = '''
tell application "System Events"
key code 9 using command down
delay 0.5
key code 36 using command down
end tell
'''
result = subprocess.run(['osascript', '-e', paste_script],
capture_output=True, text=True)
return result.returncode == 0
except Exception as e:
self.log_message(f"送信エラー: {str(e)}")
return False
def queue_sending_loop(self):
"""キュー送信ループ"""
if not self.queue_items:
self.log_message("送信するキューがありません")
self.stop_sending()
return
for i in range(5, 0, -1):
if not self.is_running:
return
self.update_status(f"送信開始まで {i} 秒...", "warning")
self.root.update()
time.sleep(1)
sent_count = 0
total_items = len(self.queue_items)
while self.is_running and self.queue_items:
try:
current_item = self.queue_items[0]
text = current_item['text']
wait_time = current_item['wait_time']
sent_count += 1
current_item['status'] = '送信中'
self.refresh_queue_display()
self.update_status(f"送信中... ({sent_count}/{total_items})", "normal")
if self.send_text(text):
preview = text[:50] + "..." if len(text) > 50 else text
self.log_message(f"送信完了 ({sent_count}件目): {preview}")
history_item = {
'text': text,
'wait_time': wait_time,
'timestamp': datetime.now().strftime("%H:%M:%S")
}
self.sent_history.append(history_item)
history_preview = text[:30] + "..." if len(text) > 30 else text
self.history_tree.insert('', 0, values=(
history_item['timestamp'],
history_preview,
f"{wait_time}秒"
))
del self.queue_items[0]
self.refresh_queue_display()
if self.queue_items:
for i in range(wait_time):
if not self.is_running:
return
remaining = wait_time - i
remaining_items = len(self.queue_items)
self.update_status(f"次回送信まで {remaining} 秒... (残り{remaining_items}件)", "normal")
self.root.update()
time.sleep(1)
else:
self.log_message("送信に失敗しました。停止します。")
current_item['status'] = '送信失敗'
self.refresh_queue_display()
break
except Exception as e:
self.log_message(f"送信エラー: {str(e)}")
if self.queue_items:
self.queue_items[0]['status'] = '送信失敗'
self.refresh_queue_display()
break
self.log_message(f"キュー送信完了: {sent_count}件送信しました")
self.stop_sending()
def start_queue_sending(self):
"""キュー送信開始"""
if self.is_running:
return
if not self.queue_items:
messagebox.showwarning("警告", "送信キューが空です")
return
target_app = self.target_app_name if self.target_app_name else self.selected_app_name
if not target_app and self.method_var.get() != "pyautogui":
messagebox.showwarning("警告", "送信先アプリを指定してください\n「送信先指定」ボタンを使用してください")
return
self.is_running = True
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.log_message("キュー送信を開始します...")
self.thread = threading.Thread(target=self.queue_sending_loop, daemon=True)
self.thread.start()
def stop_sending(self):
"""送信停止"""
self.is_running = False
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.update_status("停止中...", "warning")
self.log_message("送信を停止しました")
self.update_status("待機中", "normal")
def test_send(self):
"""テスト送信"""
text = self.text_area.get("1.0", tk.END).strip()
if not text:
messagebox.showwarning("警告", "送信する文章を入力してください")
return
self.log_message("3秒後にテスト送信します...")
def do_test_send():
if self.send_text(text):
self.log_message("テスト送信完了")
else:
self.log_message("テスト送信失敗")
self.root.after(3000, do_test_send)
def main():
try:
import pyautogui
import pyperclip
except ImportError as e:
messagebox.showerror("エラー", f"必要なライブラリがインストールされていません:\n{str(e)}\n\npip install pyautogui pyperclip")
return
if not QUARTZ_AVAILABLE:
print("警告: pyobjcライブラリが見つかりません。一部機能が制限されます。")
print("フル機能を使用するには: pip install pyobjc")
root = tk.Tk()
app = AutoChatSender(root)
try:
root.mainloop()
except KeyboardInterrupt:
print("アプリケーションを終了します")
if __name__ == "__main__":
main()
show less