Threading model
Meikipop uses a multi-threaded design where each major component runs in its own daemon thread. All threads share aSharedState object that coordinates communication through thread-safe queues and events.
SharedState class
TheSharedState class is the central coordination point for all threads:
running: Global flag to signal all threads to terminate gracefullyscreenshot_trigger_event: Signals when a new screenshot should be captured- Queues: Custom
LatestValueQueueinstances that only keep the most recent value screen_lock: Prevents the popup from being included in screenshots
LatestValueQueue
A specialized queue that only stores the most recent value:Core components
Meikipop consists of six main threaded components that communicate through the shared state:screenshot_trigger_eventhit_scan_queuewhile self.shared_state.running:
if not config.is_enabled:
time.sleep(0.1)
continue
current_mouse_pos = self.mouse_controller.position
hotkey_is_pressed = self.keyboard_controller.is_hotkey_pressed()
# trigger screenshots + ocr in manual mode
if hotkey_is_pressed and not hotkey_was_pressed and not config.auto_scan_mode:
self.shared_state.screenshot_trigger_event.set()
# trigger hit_scans + lookups
if current_mouse_pos != last_mouse_pos:
self.shared_state.hit_scan_queue.put((False, None))
screenshot_trigger_eventmss libraryocr_queuewhile self.shared_state.running:
self.shared_state.screenshot_trigger_event.wait()
self.shared_state.screenshot_trigger_event.clear()
with self.shared_state.screen_lock:
screenshot = self.take_screenshot()
img = Image.frombytes("RGB", screenshot.size, screenshot.bgra, "raw", "BGRX")
self.shared_state.ocr_queue.put(img)
src/ocr/providers/ocr_queuehit_scan_queuewhile self.shared_state.running:
screenshot = self.shared_state.ocr_queue.get()
start_time = time.perf_counter()
ocr_result = self.ocr_backend.scan(screenshot)
logger.info(
f"{self.ocr_backend.NAME} found {len(ocr_result)} paragraphs in {(time.perf_counter() - start_time):.3f}s."
)
self.shared_state.hit_scan_queue.put((True, ocr_result))
OCR providers are discovered dynamically by scanning
src/ocr/providers/ for subdirectories containing classes that inherit from OcrProvider.hit_scan_queuelookup_queuewhile self.shared_state.running:
is_ocr_result_updated, new_ocr_result = self.shared_state.hit_scan_queue.get()
if is_ocr_result_updated:
self.last_ocr_result = new_ocr_result
hit_scan_result = self.hit_scan(self.last_ocr_result) if self.last_ocr_result else None
self.shared_state.lookup_queue.put(hit_scan_result)
lookup_queuewhile self.shared_state.running:
hit_result = self.shared_state.lookup_queue.get()
# skip lookup if hit_result didnt change
if hit_result == self.last_hit_result:
continue
self.last_hit_result = hit_result
lookup_result = self.lookup(self.last_hit_result) if self.last_hit_result else None
self.popup_window.set_latest_data(lookup_result)
screen_lock when visible to prevent self-capturedef process_latest_data_loop(self):
latest_data = self.get_latest_data()
if latest_data and latest_data != self._last_latest_data:
full_html, new_size = self._calculate_content_and_size_char_count(latest_data)
self.display_label.setText(full_html)
self.setFixedSize(new_size)
if self._latest_data and self.input_loop.is_virtual_hotkey_down():
self.show_popup()
else:
self.hide_popup()
TrayIcon (non-threaded)
The system tray icon provides user interaction: Features:- Settings dialog access
- OCR provider selection
- Scan mode toggle (manual/auto)
- Scan area selection (region/screens)
- Pause/resume functionality
- Quit application
src/gui/tray.py:28):
Application lifecycle
The main application flow (fromsrc/main.py:44):
shared_state = SharedState()
input_loop = InputLoop(shared_state)
popup_window = Popup(shared_state, input_loop)
screen_manager = ScreenManager(shared_state, input_loop)
lookup = Lookup(shared_state, popup_window)
ocr_processor = OcrProcessor(shared_state, screen_manager)
hit_scanner = HitScanner(shared_state, input_loop, screen_manager)
tray_icon = TrayIcon(screen_manager, ocr_processor, popup_window, input_loop, lookup)
Data flow diagram
Here’s how data flows through the system:All threads are daemon threads, meaning they will automatically terminate when the main thread exits.
Thread safety
Meikipop uses several strategies to ensure thread safety:- LatestValueQueue: Thread-safe queue implementation with internal locking
- screen_lock: RLock prevents popup from being captured in screenshots
- _data_lock: Protects popup’s latest data from concurrent access
- Daemon threads: All worker threads are marked as daemon to ensure clean shutdown
- Atomic operations: Config changes are atomic and use file-based persistence