Update borealis_overlay.py

This commit is contained in:
Nicole Rappe 2025-02-11 00:33:08 -07:00
parent 943967be60
commit 78a3643742

View File

@ -1,21 +1,92 @@
#!/usr/bin/env python3
"""
Project Borealis (Fixed Progress Bar)
=====================================
Uses Tesseract OCR only, storing up to 7 data points.
Rollover logic (reset if new < old).
Skips duplicates.
Shows a Rich table of historical data (2+ points).
Then displays a Rich progress bar for the current EXP, plus predicted time to level
in hours/min/seconds if over 60 minutes.
Key fix: we use "with Progress(...) as progress:" *without* transient,
so the bar remains in the console after the update.
"""
import sys import sys
import os
import time
import re
import pytesseract import pytesseract
import easyocr
import numpy as np import numpy as np
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QCheckBox, QTextEdit, QComboBox, QVBoxLayout
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QRect, QPoint, QTimer from PyQt5.QtCore import Qt, QRect, QPoint, QTimer
from PyQt5.QtGui import QPainter, QPen, QColor, QFont from PyQt5.QtGui import QPainter, QPen, QColor, QFont
from PIL import Image, ImageGrab, ImageEnhance, ImageFilter # For screen capture and image processing from PIL import Image, ImageGrab, ImageEnhance, ImageFilter
HANDLE_SIZE = 10 # Size of the resize handle squares from rich.console import Console
LABEL_HEIGHT = 20 # Height of the label area above the rectangle from rich.table import Table
DEFAULT_WIDTH = 150 # Default width for the regions from rich.progress import Progress, BarColumn, TextColumn
DEFAULT_HEIGHT = 50 # Default height for the regions
DEFAULT_SPACING = 20 # Default horizontal spacing between regions
# Set the path for tesseract manually # ---------------- [ Global Config ] ----------------
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
OCR_ENGINE = "Tesseract" # Hard-coded since we removed EasyOCR
POLLING_RATE_MS = 1000
MAX_DATA_POINTS = 7
GREEN_HEADER_STYLE = "bold green"
HANDLE_SIZE = 10
LABEL_HEIGHT = 20
DEFAULT_WIDTH = 150
DEFAULT_HEIGHT = 50
def sanitize_experience_string(raw_text):
"""Extract a float from raw OCR text, removing extraneous symbols."""
text_no_percent = raw_text.replace('%', '')
text_no_spaces = text_no_percent.replace(' ', '')
cleaned = re.sub(r'[^0-9\.]', '', text_no_spaces)
match = re.search(r'\d+(?:\.\d+)?', cleaned)
if not match:
return None
val = float(match.group(0))
return round(val, 4)
def format_experience_value(value):
"""Format float to 'XX.XXXX' with leading zeros if needed."""
if value < 0:
value = 0
elif value > 100:
value = 100
float_4 = round(value, 4)
raw_str = f"{float_4:.4f}"
int_part, dec_part = raw_str.split('.')
if int_part == "100":
pass
elif len(int_part) == 1 and int_part != "0":
int_part = "0" + int_part
elif int_part == "0":
int_part = "00"
return f"{int_part}.{dec_part}"
def format_duration(seconds):
"""
Convert total seconds into hours / min / sec:
- If hours > 0 => "Xh Ym Zs"
- Otherwise => "Xm Ys"
"""
seconds = int(seconds)
hours = seconds // 3600
leftover = seconds % 3600
mins = leftover // 60
secs = leftover % 60
if hours > 0:
return f"{hours}h {mins}m {secs}s"
else:
return f"{mins}m {secs}s"
class Region: class Region:
def __init__(self, x, y, label="Region", color=QColor(0, 0, 255)): def __init__(self, x, y, label="Region", color=QColor(0, 0, 255)):
@ -25,81 +96,68 @@ class Region:
self.h = DEFAULT_HEIGHT self.h = DEFAULT_HEIGHT
self.label = label self.label = label
self.color = color self.color = color
self.visible = True # Track the visibility of the region self.visible = True
self.data = "" # Store OCR data for this region self.data = ""
def rect(self): def rect(self):
return QRect(self.x, self.y, self.w, self.h) return QRect(self.x, self.y, self.w, self.h)
def label_rect(self): def label_rect(self):
"""The rectangle representing the label area."""
return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT) return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT)
def resize_handles(self): def resize_handles(self):
"""Calculate the positions of the resize handles."""
return [ return [
QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # Top-left QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # Top-right QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # Bottom-left QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE), # Bottom-right QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
] ]
class OverlayCanvas(QWidget): class OverlayCanvas(QWidget):
"""
A canvas to draw, resize, and interact with rectangular regions.
"""
def __init__(self, regions, parent=None): def __init__(self, regions, parent=None):
super().__init__(parent) super().__init__(parent)
self.regions = regions self.regions = regions
self.edit_mode = False self.edit_mode = True
self.selected_region = None self.selected_region = None
self.selected_handle = None # Track which handle is being dragged self.selected_handle = None
self.drag_offset = QPoint() self.drag_offset = QPoint()
def paintEvent(self, event): def paintEvent(self, event):
painter = QPainter(self) painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing)
for region in self.regions: for region in self.regions:
if region.visible: # Only draw visible regions if region.visible:
# Draw the rectangle
pen = QPen(region.color) pen = QPen(region.color)
pen.setWidth(3) pen.setWidth(3)
painter.setPen(pen) painter.setPen(pen)
painter.drawRect(region.x, region.y, region.w, region.h) painter.drawRect(region.x, region.y, region.w, region.h)
# Draw the label above the rectangle, aligned with the left edge of the region
painter.setFont(QFont("Arial", 12, QFont.Bold)) painter.setFont(QFont("Arial", 12, QFont.Bold))
painter.setPen(region.color) painter.setPen(region.color)
painter.drawText(region.x, region.y - 5, region.label) # Aligned to the left of the region painter.drawText(region.x, region.y - 5, region.label)
# Draw resize handles if in edit mode
if self.edit_mode: if self.edit_mode:
for handle in region.resize_handles(): for handle in region.resize_handles():
painter.fillRect(handle, region.color) painter.fillRect(handle, region.color)
def mousePressEvent(self, event): def mousePressEvent(self, event):
if not self.edit_mode: if not self.edit_mode:
return # Ignore clicks if not in edit mode return
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
for region in reversed(self.regions): # Check regions from topmost to bottommost for region in reversed(self.regions):
# Check if a resize handle is clicked # Check handles
for i, handle in enumerate(region.resize_handles()): for i, handle in enumerate(region.resize_handles()):
if handle.contains(event.pos()): if handle.contains(event.pos()):
self.selected_region = region self.selected_region = region
self.selected_handle = i self.selected_handle = i
return return
# Check label or rect
# Check if the label area is clicked (for dragging)
if region.label_rect().contains(event.pos()): if region.label_rect().contains(event.pos()):
self.selected_region = region self.selected_region = region
self.selected_handle = None # No resize handle, just dragging the rectangle self.selected_handle = None
self.drag_offset = event.pos() - QPoint(region.x, region.y) self.drag_offset = event.pos() - QPoint(region.x, region.y)
return return
# Check if the main rectangle is clicked (fallback for dragging)
if region.rect().contains(event.pos()): if region.rect().contains(event.pos()):
self.selected_region = region self.selected_region = region
self.selected_handle = None self.selected_handle = None
@ -111,274 +169,209 @@ class OverlayCanvas(QWidget):
return return
if self.selected_handle is None: if self.selected_handle is None:
# Dragging the entire rectangle # Drag rectangle
self.selected_region.x = event.x() - self.drag_offset.x() self.selected_region.x = event.x() - self.drag_offset.x()
self.selected_region.y = event.y() - self.drag_offset.y() self.selected_region.y = event.y() - self.drag_offset.y()
else: else:
# Resizing the rectangle # Resize
sr = self.selected_region
if self.selected_handle == 0: # Top-left if self.selected_handle == 0: # Top-left
self.selected_region.w += self.selected_region.x - event.x() sr.w += sr.x - event.x()
self.selected_region.h += self.selected_region.y - event.y() sr.h += sr.y - event.y()
self.selected_region.x = event.x() sr.x = event.x()
self.selected_region.y = event.y() sr.y = event.y()
elif self.selected_handle == 1: # Top-right elif self.selected_handle == 1: # Top-right
self.selected_region.w = event.x() - self.selected_region.x sr.w = event.x() - sr.x
self.selected_region.h += self.selected_region.y - event.y() sr.h += sr.y - event.y()
self.selected_region.y = event.y() sr.y = event.y()
elif self.selected_handle == 2: # Bottom-left elif self.selected_handle == 2: # Bottom-left
self.selected_region.w += self.selected_region.x - event.x() sr.w += sr.x - event.x()
self.selected_region.h = event.y() - self.selected_region.y sr.h = event.y() - sr.y
self.selected_region.x = event.x() sr.x = event.x()
elif self.selected_handle == 3: # Bottom-right elif self.selected_handle == 3: # Bottom-right
self.selected_region.w = event.x() - self.selected_region.x sr.w = event.x() - sr.x
self.selected_region.h = event.y() - self.selected_region.y sr.h = event.y() - sr.y
# Prevent negative width/height sr.w = max(sr.w, 10)
self.selected_region.w = max(self.selected_region.w, 10) sr.h = max(sr.h, 10)
self.selected_region.h = max(self.selected_region.h, 10) self.update()
self.update() # Trigger a repaint
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
if not self.edit_mode: if not self.edit_mode:
return return
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
self.selected_region = None self.selected_region = None
self.selected_handle = None # Deselect handle self.selected_handle = None
class BorealisOverlay(QWidget): class BorealisOverlay(QWidget):
"""
Project Borealis with Tesseract + Rich table + Rich progress bar,
properly displayed with "with Progress(...) as progress".
"""
def __init__(self): def __init__(self):
super().__init__() super().__init__()
# Set window properties to cover the full screen # Fullscreen overlay
screen_geometry = QApplication.primaryScreen().geometry() screen_geometry = QApplication.primaryScreen().geometry()
self.setGeometry(screen_geometry) self.setGeometry(screen_geometry)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground, True) # Transparent background self.setAttribute(Qt.WA_TranslucentBackground, True)
# Create regions to draw and interact with, all regions have the same size and are moved higher up
self.regions = [ self.regions = [
Region(250, 50, label="Experience"), # Moved slightly to the right Region(250, 50, label="Experience"),
Region(450, 50, label="Region 02"), # Moved slightly to the right
Region(650, 50, label="Region 03") # Moved slightly to the right
] ]
# Create canvas and attach to window
self.canvas = OverlayCanvas(self.regions, self) self.canvas = OverlayCanvas(self.regions, self)
self.canvas.setGeometry(self.rect()) # Match the canvas size to the full window self.canvas.setGeometry(self.rect())
# Add title, Edit Mode UI, and buttons (Overlay Visibility checkbox) # Tesseract
self.init_ui() self.engine = pytesseract
# Timer for polling OCR data self.points = []
self.timer = QTimer(self) self.timer = QTimer(self)
self.timer.timeout.connect(self.collect_ocr_data) self.timer.timeout.connect(self.collect_ocr_data)
self.timer.start(1000) # Poll every second (default) self.timer.start(POLLING_RATE_MS)
self.reader = None # Default OCR reader (None until an engine is selected)
def init_ui(self):
"""Initialize UI components."""
# Title label
self.title_label = QLabel("Borealis Overlay", self)
self.title_label.setStyleSheet("QLabel { color: white; font-size: 20px; font-weight: bold; }") # Adjusted title size
self.title_label.move(10, 5)
# OCR Engine label and selection dropdown
self.engine_label = QLabel("OCR Engine:", self)
self.engine_label.setStyleSheet("QLabel { color: white; font-size: 14px; }")
self.engine_label.move(10, 60) # Moved OCR Engine label slightly higher
self.engine_dropdown = QComboBox(self)
self.engine_dropdown.setStyleSheet("""
QComboBox {
color: white;
background-color: #2b2b2b;
border: 1px solid #3c3f41;
font-size: 14px;
}
QComboBox QAbstractItemView {
color: white;
background-color: #2b2b2b;
}
""")
self.engine_dropdown.addItem("Select OCR Engine") # Placeholder option
self.engine_dropdown.addItem("Tesseract") # Only Tesseract for now
self.engine_dropdown.addItem("EasyOCR") # Adding EasyOCR option
self.engine_dropdown.move(100, 90) # Dropdown moved slightly up and aligned with label
self.engine_dropdown.currentIndexChanged.connect(self.on_engine_selected)
# Polling rate dropdown
self.polling_rate_label = QLabel("Polling Rate:", self)
self.polling_rate_label.setStyleSheet("QLabel { color: white; font-size: 14px; }")
self.polling_rate_label.move(10, 120)
self.polling_rate_dropdown = QComboBox(self)
self.polling_rate_dropdown.setStyleSheet("""
QComboBox {
color: white;
background-color: #2b2b2b;
border: 1px solid #3c3f41;
font-size: 14px;
}
QComboBox QAbstractItemView {
color: white;
background-color: #2b2b2b;
}
""")
self.polling_rate_dropdown.addItem("0.1 Seconds")
self.polling_rate_dropdown.addItem("0.5 Seconds")
self.polling_rate_dropdown.addItem("1 Second")
self.polling_rate_dropdown.addItem("2 Seconds")
self.polling_rate_dropdown.addItem("5 Seconds")
self.polling_rate_dropdown.move(100, 150) # Dropdown moved slightly up
self.polling_rate_dropdown.currentIndexChanged.connect(self.on_polling_rate_selected)
# Options label
self.options_label = QLabel("Options", self)
self.options_label.setStyleSheet("QLabel { color: white; font-size: 16px; font-weight: bold; }")
self.options_label.move(10, 180) # Positioned above checkboxes
# Edit mode checkbox
self.mode_toggle = QCheckBox("Edit Mode", self)
self.mode_toggle.setStyleSheet("QCheckBox { color: white; }")
self.mode_toggle.move(10, 210)
self.mode_toggle.stateChanged.connect(self.toggle_edit_mode)
# Overlay Visibility checkbox
self.visibility_checkbox = QCheckBox("Overlay Visibility", self)
self.visibility_checkbox.setStyleSheet("QCheckBox { color: white; }")
self.visibility_checkbox.move(10, 240) # Positioned below Edit Mode
self.visibility_checkbox.setChecked(True) # Default to visible
self.visibility_checkbox.stateChanged.connect(self.toggle_overlay_visibility)
# Collect Data checkbox for OCR functionality (disabled initially)
self.collect_data_checkbox = QCheckBox("Collect Data", self)
self.collect_data_checkbox.setStyleSheet("QCheckBox { color: white; }")
self.collect_data_checkbox.move(10, 270) # Positioned below Overlay Visibility
self.collect_data_checkbox.setEnabled(False) # Initially disabled
self.collect_data_checkbox.stateChanged.connect(self.toggle_ocr)
# Data Collection Output label
self.output_label = QLabel("Data Collection Output:", self)
self.output_label.setStyleSheet("QLabel { color: white; font-size: 14px; font-weight: bold; }")
self.output_label.move(10, 330) # Moved down by 50px
# Text area for OCR data display (with transparent background)
self.ocr_display = QTextEdit(self)
self.ocr_display.setStyleSheet("QTextEdit { color: white; background-color: transparent; font-size: 14px; }")
self.ocr_display.setReadOnly(True)
self.ocr_display.setGeometry(10, 360, 300, 400)
def on_engine_selected(self):
"""Enable the Collect Data checkbox when an OCR engine is selected."""
selected_engine = self.engine_dropdown.currentText()
if selected_engine == "Tesseract":
self.reader = pytesseract
elif selected_engine == "EasyOCR":
self.reader = easyocr.Reader(['en']) # Initialize EasyOCR reader for English language
elif selected_engine == "Select OCR Engine":
self.reader = None
if self.reader:
self.collect_data_checkbox.setEnabled(True)
else:
self.collect_data_checkbox.setEnabled(False)
def on_polling_rate_selected(self):
"""Update the polling rate based on dropdown selection."""
polling_rate = self.polling_rate_dropdown.currentText()
if polling_rate == "0.1 Seconds":
self.timer.setInterval(100)
elif polling_rate == "0.5 Seconds":
self.timer.setInterval(500)
elif polling_rate == "1 Second":
self.timer.setInterval(1000)
elif polling_rate == "2 Seconds":
self.timer.setInterval(2000)
elif polling_rate == "5 Seconds":
self.timer.setInterval(5000)
def toggle_edit_mode(self, state):
"""Enable or disable edit mode for dragging and resizing rectangles."""
editing = (state == 2)
self.canvas.edit_mode = editing # Pass the state to the canvas
def toggle_overlay_visibility(self):
"""Toggle the visibility of the regions."""
visible = self.visibility_checkbox.isChecked()
for region in self.regions:
region.visible = visible # Toggle visibility based on checkbox
self.update() # Trigger a repaint
def toggle_ocr(self, state):
"""Enable or disable OCR collection."""
if state == Qt.Checked:
self.collect_ocr_data()
else:
self.clear_ocr_data()
def collect_ocr_data(self): def collect_ocr_data(self):
"""Collect OCR data from each visible region.""" if not self.engine:
if self.collect_data_checkbox.isChecked() and self.reader: # Only collect data if checkbox is checked return
for region in self.regions:
if region.visible:
# Capture the image of the region
screenshot = ImageGrab.grab(bbox=(region.x, region.y, region.x + region.w, region.y + region.h))
# Preprocess the image (Convert to grayscale and apply threshold) for region in self.regions:
processed_image = self.preprocess_image(screenshot) if region.visible:
screenshot = ImageGrab.grab(
bbox=(region.x, region.y, region.x + region.w, region.y + region.h)
)
processed_image = self.preprocess_image(screenshot)
# Convert the processed image to a numpy array for EasyOCR text = pytesseract.image_to_string(processed_image, config='--psm 6 --oem 1')
numpy_image = np.array(processed_image) region.data = text.strip()
# Perform OCR on the preprocessed numpy image if region.label.lower() == "experience":
if self.reader == pytesseract: val = sanitize_experience_string(region.data)
text = pytesseract.image_to_string(processed_image, config='--psm 6 --oem 1') if val is not None:
self.update_points(val)
region.data = format_experience_value(val) + "%"
else: else:
# Get text from EasyOCR region.data = "N/A"
results = self.reader.readtext(numpy_image)
if results: self.display_ocr_data_in_terminal()
text = results[0][1] # If OCR detects text, use it
else:
text = "No text detected" # Fallback if no text is detected
region.data = text.strip()
self.display_ocr_data()
def preprocess_image(self, image): def preprocess_image(self, image):
"""Preprocess the image to enhance text recognition.""" gray = image.convert("L")
# Convert image to grayscale scaled = gray.resize(
gray_image = image.convert("L") (gray.width * 3, gray.height * 3),
resample=Image.Resampling.LANCZOS
)
thresh = scaled.point(lambda p: p > 200 and 255)
return thresh.filter(ImageFilter.MedianFilter(3))
# Apply threshold to make the image binary (black and white) def update_points(self, new_val):
threshold_image = gray_image.point(lambda p: p > 200 and 255) now = time.time()
if self.points:
_, last_v = self.points[-1]
if abs(new_val - last_v) < 1e-6:
return
if new_val < last_v:
self.points.clear()
self.points.append((now, new_val))
if len(self.points) > MAX_DATA_POINTS:
self.points.pop(0)
# Apply noise reduction (filter) def compute_time_to_100(self):
processed_image = threshold_image.filter(ImageFilter.MedianFilter(3)) """Return integer seconds until 100% or None if not feasible."""
return processed_image n = len(self.points)
if n < 2:
return None
first_t, first_v = self.points[0]
last_t, last_v = self.points[-1]
diff_v = last_v - first_v
if diff_v <= 0:
return None
def clear_ocr_data(self): steps = n - 1
"""Clear the displayed OCR data.""" total_time = last_t - first_t
self.ocr_display.clear() if total_time <= 0:
return None
def display_ocr_data(self): avg_change = diff_v / steps
"""Display OCR data in the text area on the left-hand side.""" remain = 100.0 - last_v
ocr_text = "" if remain <= 0:
for region in self.regions: return None
ocr_text += f"{region.label} Output:\n{region.data}\n\n" # Updated headers with "Output"
self.ocr_display.setText(ocr_text)
avg_time = total_time / steps
rate_per_s = avg_change / avg_time if avg_time > 0 else 0
if rate_per_s <= 0:
return None
return int(remain / rate_per_s)
def display_ocr_data_in_terminal(self):
console = Console()
os.system('cls' if os.name == 'nt' else 'clear')
console.print("[bold white]Project Borealis[/bold white]")
console.print("[dim]Flyff Information Overlay[/dim]\n")
console.print(f"[bold]OCR Engine[/bold]: {OCR_ENGINE}")
console.print(f"[bold]Data Polling Rate[/bold]: {POLLING_RATE_MS / 1000}s\n")
# Build the historical table
table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None)
table.add_column("Historical EXP", justify="center", style="green")
table.add_column("Time Since Last Kill", justify="center", style="green")
table.add_column("Average EXP Per Kill", justify="center", style="green")
table.add_column("Average Time Between Kills", justify="center", style="green")
n = len(self.points)
if n == 1:
t0, v0 = self.points[0]
exp_str = f"[green]{format_experience_value(v0)}%[/green]"
table.add_row(exp_str, "N/A", "N/A", "N/A")
else:
for i in range(1, n):
t_cur, v_cur = self.points[i]
t_prev, v_prev = self.points[i - 1]
exp_str = f"[green]{format_experience_value(v_cur)}%[/green]"
delta_t = t_cur - t_prev
t_since_str = f"{delta_t:.1f}s"
diff_v = v_cur - self.points[0][1]
steps = i
avg_exp_str = f"{diff_v/steps:.4f}%"
total_time = t_cur - self.points[0][0]
avg_kill_time = total_time / steps
avg_time_str = f"{avg_kill_time:.1f}s"
table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str)
console.print(table)
console.print()
# Prepare the progress bar
current_exp = self.points[-1][1] if self.points else 0.0
secs_left = self.compute_time_to_100()
time_str = format_duration(secs_left) if secs_left is not None else "???"
# We'll display a single progress bar line in a context manager
# Setting transient=False ensures it remains in the console
with Progress(
TextColumn("[bold white]Predicted Time to Level:[/bold white] "),
BarColumn(bar_width=30),
TextColumn(" [green]{task.percentage:>5.2f}%[/green] "),
TextColumn(f"[orange]{time_str}[/orange] until 100%", justify="right"),
console=console,
transient=False,
) as progress:
task_id = progress.add_task("", total=100, completed=current_exp)
# Force a manual refresh so the bar appears immediately
progress.refresh()
def main(): def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
window = BorealisOverlay() window = BorealisOverlay()
window.setWindowTitle("Borealis Overlay") # Set application window title window.setWindowTitle("Project Borealis Overlay (Fixed Progress Bar)")
window.show() # Explicitly show the window window.show()
sys.exit(app.exec_()) sys.exit(app.exec_())