├── .gitignore ├── LICENSE.txt ├── README.md ├── assets ├── message-dots.png ├── model_ss.png ├── network_tab.png ├── notes.png ├── ocr_lang_detect.png ├── ocr_output.txt ├── pc_tab.png ├── screenshot.png ├── square-letter-x.png ├── terminal.png ├── terminal_tab.png ├── test.txt └── zoom-scan.png ├── chat_app.py ├── chat_window.py ├── langs_import.py ├── main.py ├── notes_window.py ├── ocr.py └── terminal_monitor.py /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | build 3 | __pycache__ 4 | build.spec 5 | todo 6 | test 7 | start.bat -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 PyChat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ollama Chat Application This is a simple chat application built using **PyQt6** and **Tkinter** that integrates with the **Ollama** model server. It also offers a UI tool to utilize **OCR** for text extraction by dragging a selection box and having OCR analyze the screenshot. Additionally, it features note-saving functionality. You can use the app either by cloning, downloading the zip, or by downloading the `.exe` 2 | 3 | ![Chat Application Interface](./assets/model_ss.png) 4 | ![Chat Application Interface](./assets/ocr_lang_detect.png) 5 | ![Chat Application Interface](./assets/pc_tab.png) 6 | ![Chat Application Interface](./assets/terminal_tab.png) 7 | ![Chat Application Interface](./assets/network_tab.png) 8 | 9 | ## Installation 1. **Download the Zip or Clone**: - Navigate to the root directory and run ``py main.py``. Install dependencies as needed. 10 | 11 | 2. **To Use the OCR Tool**: 12 | 13 | - First, install Google's Tesseract OCR model [here](https://github.com/tesseract-ocr/tesseract). 14 | 15 | - This app uses **pytesseract**; more info can be found [here](https://github.com/h/pytesseract). 16 | 17 | - Set up your system's environmental variables as necessary. 18 | 19 | - By default, the software is installed in `C:\Program Files\Tesseract-OCR\tesseract.exe` if you're running Windows. 20 | - Install the languages you want supported for detection from https://github.com/tesseract-ocr/tessdata 21 | 22 | - When using the OCR, every screenshot you process is automatically injected into the chat. This works great for translating using your Ollama model. Hit clear to prevent the OCR text from being injected. 23 | - When detecting languages, initially it will take several seconds depending on how many languages you have loaded. Once language is detected change the drop down menu to the language your will be using the OCR on for faster recognition. 24 | 25 | 3. **Starting the Ollama Server**: 26 | 27 | - This app automatically runs a script to start the Ollama server upon starting the app. You can change this in the `main.py`. 28 | 29 | ## New Features 30 | 31 | ### OCR Language Detection and Translation 32 | 33 | - **Language Detection**: The app now detects the language of the text extracted through OCR using a built-in language detection library. 34 | - **Translation**: After detecting the language, you can send the extracted text to the Ollama server for translation into your desired language. 35 | 36 | ### System Monitoring 37 | 38 | - **PC Monitor/Terminal/Network Stats**: 39 | - The application now includes a feature to monitor and display system statistics such as CPU usage, memory usage, and network statistics. 40 | - Access these stats from a dedicated tab in the application. 41 | - Embededd windows terminal 42 | 43 | ## Note 44 | 45 | Currently, I don't have an updated `.exe` for this due to the OCR binary complications. The `.exe` available in the release works; it just lacks newer updated functionalities such as OCR and notes. Also for now this is only meant for Windows 46 | 47 | -------------------------------------------------------------------------------- /assets/message-dots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/message-dots.png -------------------------------------------------------------------------------- /assets/model_ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/model_ss.png -------------------------------------------------------------------------------- /assets/network_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/network_tab.png -------------------------------------------------------------------------------- /assets/notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/notes.png -------------------------------------------------------------------------------- /assets/ocr_lang_detect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/ocr_lang_detect.png -------------------------------------------------------------------------------- /assets/ocr_output.txt: -------------------------------------------------------------------------------- 1 | 元 来 日 本 語 は 漢 文 に 倣 い 、 文 字 を 上 2 | 劣 口 下 八 、 細 左 行 客 右 力 口 左 人 立 週 3 | め て 表 記 を 行 っ て い た 。 漢 字 と 仮 名 4 | の 筆 順 も 縦 書 き を 前 提 と し て お り 、 5 | 横 書 き 不 能 な 書 体 も 存 在 す る 。 6 | -------------------------------------------------------------------------------- /assets/pc_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/pc_tab.png -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/screenshot.png -------------------------------------------------------------------------------- /assets/square-letter-x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/square-letter-x.png -------------------------------------------------------------------------------- /assets/terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/terminal.png -------------------------------------------------------------------------------- /assets/terminal_tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/terminal_tab.png -------------------------------------------------------------------------------- /assets/test.txt: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /assets/zoom-scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TLAMHutto/ollamaChat/671244e1298ef53cb9c724ba20a3fb4ca9a7a97c/assets/zoom-scan.png -------------------------------------------------------------------------------- /chat_app.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import QWidget, QHBoxLayout, QPushButton, QLabel 2 | from PyQt6.QtCore import Qt, QPoint, QSize 3 | from PyQt6.QtGui import QPixmap, QIcon 4 | from chat_window import ChatWindow 5 | from notes_window import NotesWindow 6 | from terminal_monitor import CombinedApp 7 | from ocr import OCR 8 | import threading 9 | import tkinter as tk 10 | class MinimalistChatApp(QWidget): 11 | def __init__(self): 12 | super().__init__(None, Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint) 13 | self.chat_window = None 14 | self.ocr_window = None # Add ocr_window reference 15 | self.notes_window = None 16 | self.terminal_window = None 17 | self.initUI() 18 | self.oldPos = self.pos() 19 | 20 | def initUI(self): 21 | self.setGeometry(1680, 992, 150, 25) # Increase width to accommodate new button 22 | 23 | # Create main layout 24 | main_layout = QHBoxLayout() 25 | main_layout.setContentsMargins(0, 0, 0, 0) # Remove margins 26 | 27 | self.terminal_button = QPushButton(self) 28 | self.terminal_button.setFixedSize(25, 25) 29 | 30 | self.square_button = QPushButton(self) 31 | self.square_button.setFixedSize(25, 25) 32 | 33 | self.chat_button = QPushButton(self) 34 | self.chat_button.setFixedSize(25, 25) 35 | 36 | self.notes_button = QPushButton(self) 37 | self.notes_button.setFixedSize(25, 25) 38 | 39 | self.exit_button = QPushButton(self) 40 | self.exit_button.setFixedSize(25, 25) 41 | 42 | 43 | 44 | # Load the icon images 45 | scan = QPixmap('./assets/zoom-scan.png') 46 | chat = QPixmap('./assets/message-dots.png') 47 | exit = QPixmap('./assets/square-letter-x.png') 48 | notes = QPixmap('./assets/notes.png') 49 | terminal = QPixmap('./assets/terminal.png') 50 | 51 | self.terminal_button.setIcon(QIcon(terminal)) 52 | self.terminal_button.setIconSize(QSize(20, 20)) # Set the size of the icon 53 | self.terminal_button.setStyleSheet('background-color: lightgray; border: none;') # Optional: set background color and remove border 54 | self.terminal_button.clicked.connect(self.toggle_terminal_window) # Connect button click to open OCR window 55 | 56 | # Configure square button 57 | self.square_button.setIcon(QIcon(scan)) 58 | self.square_button.setIconSize(QSize(20, 20)) # Set the size of the icon 59 | self.square_button.setStyleSheet('background-color: lightgray; border: none;') # Optional: set background color and remove border 60 | self.square_button.clicked.connect(self.open_ocr_window) # Connect button click to open OCR window 61 | 62 | # Configure chat button 63 | self.chat_button.setIcon(QIcon(chat)) 64 | self.chat_button.setIconSize(QSize(20, 20)) # Set the size of the icon 65 | self.chat_button.setStyleSheet('background-color: lightgray; border: none;') # Optional: set background color and remove border 66 | self.chat_button.clicked.connect(self.toggle_chat_window) # Connect button click to open chat window 67 | 68 | # Configure exit button 69 | self.exit_button.setIcon(QIcon(exit)) 70 | self.exit_button.setIconSize(QSize(20, 20)) # Set the size of the icon 71 | self.exit_button.setStyleSheet('background-color: lightgray; border: none;') # Optional: set background color and remove border 72 | self.exit_button.clicked.connect(self.close) # Only close the window on click 73 | 74 | # Configure notes button (Fix the connection) 75 | self.notes_button.setIcon(QIcon(notes)) 76 | self.notes_button.setIconSize(QSize(20, 20)) # Set the size of the icon 77 | self.notes_button.setStyleSheet('background-color: lightgray; border: none;') # Optional: set background color and remove border 78 | self.notes_button.clicked.connect(self.toggle_notes_window) # Connect to the correct method for toggling notes window 79 | # Add buttons to layout 80 | main_layout = QHBoxLayout(self) 81 | main_layout.addWidget(self.terminal_button) 82 | main_layout.addWidget(self.square_button) 83 | main_layout.addWidget(self.chat_button) 84 | main_layout.addWidget(self.notes_button) 85 | main_layout.addWidget(self.exit_button) 86 | 87 | 88 | def toggle_terminal_window(self): 89 | try: 90 | if self.terminal_window and self.terminal_window.isVisible(): 91 | self.terminal_window.hide() 92 | else: 93 | if not self.terminal_window: 94 | self.terminal_window = CombinedApp() 95 | self.terminal_window.show() 96 | except Exception as e: 97 | print(f"Error in toggle_terminal_window: {e}") 98 | 99 | def toggle_chat_window(self): 100 | # Close the notes window if it's open 101 | if self.notes_window and self.notes_window.isVisible(): 102 | self.notes_window.hide() 103 | 104 | # Toggle the chat window 105 | if self.chat_window and self.chat_window.isVisible(): 106 | self.chat_window.hide() 107 | else: 108 | if not self.chat_window: 109 | self.chat_window = ChatWindow(self) 110 | self.chat_window.show() 111 | 112 | def toggle_notes_window(self): 113 | # Close the chat window if it's open 114 | if self.chat_window and self.chat_window.isVisible(): 115 | self.chat_window.hide() 116 | 117 | # Toggle the notes window 118 | if self.notes_window and self.notes_window.isVisible(): 119 | self.notes_window.hide() 120 | else: 121 | if not self.notes_window: 122 | self.notes_window = NotesWindow(self) 123 | self.notes_window.show() 124 | 125 | def open_ocr_window(self): 126 | # Run Tkinter window in a separate thread 127 | def run_tkinter(): 128 | root = tk.Tk() # Create a root Tk instance 129 | root.withdraw() # Hide the root window 130 | OCR(root) # Create the OCR window 131 | root.mainloop() # Run Tkinter's event loop 132 | threading.Thread(target=run_tkinter, daemon=True).start() 133 | 134 | def mousePressEvent(self, event): 135 | self.oldPos = event.globalPosition().toPoint() 136 | 137 | def mouseMoveEvent(self, event): 138 | delta = QPoint(event.globalPosition().toPoint() - self.oldPos) 139 | self.move(self.x() + delta.x(), self.y() + delta.y()) 140 | self.oldPos = event.globalPosition().toPoint() 141 | 142 | def closeEvent(self, event): 143 | if self.chat_window: 144 | self.chat_window.close() 145 | if self.ocr_window: 146 | self.ocr_window.close() 147 | if self.notes_window: 148 | self.notes_window.close() 149 | if self.terminal_window: 150 | self.terminal_window.close() 151 | event.accept() 152 | 153 | -------------------------------------------------------------------------------- /chat_window.py: -------------------------------------------------------------------------------- 1 | 2 | import re 3 | import html 4 | from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, 5 | QLineEdit, QPushButton, QComboBox, QLabel) 6 | from PyQt6.QtGui import QTextCursor, QIcon 7 | from PyQt6.QtCore import Qt, QPoint 8 | import ollama 9 | class ChatWindow(QWidget): 10 | def __init__(self, parent=None): 11 | super().__init__(parent, Qt.WindowType.Window | Qt.WindowType.FramelessWindowHint) 12 | self.initUI() 13 | self.oldPos = self.pos() 14 | self.resizing = True 15 | self.resize_edge = None 16 | self.resize_margin = 10 # The width of the "resizable" border area 17 | self.token_count = 0 18 | def initUI(self): 19 | self.setGeometry(1620, 390, 300, 600) 20 | 21 | layout = QVBoxLayout() 22 | 23 | # Create a horizontal layout for the dropdown, refresh button, and token count 24 | top_layout = QHBoxLayout() 25 | 26 | # Add the model dropdown 27 | self.model_dropdown = QComboBox(self) 28 | self.populate_model_dropdown() 29 | top_layout.addWidget(self.model_dropdown) 30 | 31 | # Add the refresh button with an icon 32 | self.refresh_button = QPushButton(self) 33 | self.refresh_button.setIcon(QIcon.fromTheme("view-refresh")) 34 | self.refresh_button.setToolTip("Clear chat") 35 | self.refresh_button.clicked.connect(self.clear_chat) 36 | top_layout.addWidget(self.refresh_button) 37 | 38 | # Add the token count label 39 | self.token_count_label = QLabel("Tokens: 0", self) 40 | top_layout.addWidget(self.token_count_label) 41 | 42 | layout.addLayout(top_layout) 43 | 44 | # Add the chat area 45 | self.chat_area = QTextEdit(self) 46 | self.chat_area.setReadOnly(True) 47 | layout.addWidget(self.chat_area) 48 | 49 | # Add the text input at the bottom of the window 50 | self.text_input = QLineEdit(self) 51 | self.text_input.setPlaceholderText("Type your message here...") 52 | layout.addWidget(self.text_input) 53 | 54 | # Connect the return key (Enter) press to send message action 55 | self.text_input.returnPressed.connect(self.send_message) 56 | 57 | self.setLayout(layout) 58 | 59 | 60 | def send_message(self): 61 | message = self.text_input.text().strip() 62 | if message: 63 | self.append_message("User", message) 64 | self.text_input.clear() 65 | 66 | # Get the selected model 67 | model = self.model_dropdown.currentText() 68 | 69 | try: 70 | # Read OCR text from file 71 | ocr_text = "" 72 | try: 73 | with open("./assets/ocr_output.txt", "r", encoding="utf-8") as f: 74 | ocr_text = f.read().strip() 75 | except FileNotFoundError: 76 | pass # If the file doesn't exist, we'll just use an empty string 77 | 78 | # Combine user message with OCR text 79 | combined_message = f"{message}\n\nOCR Text:\n{ocr_text}" if ocr_text else message 80 | 81 | # Send message to Ollama and get response 82 | response = ollama.chat(model=model, messages=[ 83 | { 84 | 'role': 'user', 85 | 'content': combined_message, 86 | }, 87 | ]) 88 | 89 | # Display the AI's response 90 | ai_message = response['message']['content'] 91 | self.append_message(f"AI ({model})", ai_message) 92 | except Exception as e: 93 | self.append_message("Error", f"Failed to get response from {model}. {str(e)}") 94 | 95 | def populate_model_dropdown(self): 96 | try: 97 | models_dict = ollama.list() 98 | models = models_dict.get('models', []) 99 | for model in models: 100 | model_name = model.get("name", "No name key") 101 | self.model_dropdown.addItem(model_name) 102 | except Exception as e: 103 | self.append_message("Error", f"Error fetching models: {str(e)}") 104 | 105 | def append_message(self, sender, message): 106 | # Format the message with code blocks 107 | formatted_message = self.format_message(message) 108 | 109 | # Append the formatted message to the chat area 110 | self.chat_area.append(f"{sender}: {formatted_message}") 111 | 112 | # Update token count 113 | self.update_token_count(message) 114 | 115 | # Scroll to the bottom of the chat area 116 | self.chat_area.moveCursor(QTextCursor.MoveOperation.End) 117 | self.chat_area.ensureCursorVisible() 118 | 119 | def format_message(self, message): 120 | # Use regex to find code blocks (text between triple backticks) 121 | code_block_pattern = r'```([\s\S]*?)```' 122 | 123 | def replace_code_block(match): 124 | # Escape the code for HTML rendering 125 | code = html.escape(match.group(1).strip()) 126 | return f'
{code}
' 127 | 128 | # Replace code blocks with formatted HTML 129 | formatted_message = re.sub(code_block_pattern, replace_code_block, message) 130 | 131 | # Replace newlines with
tags for proper HTML rendering 132 | formatted_message = formatted_message.replace('\n', '
') 133 | 134 | return formatted_message 135 | 136 | def clear_chat(self): 137 | self.chat_area.clear() 138 | self.token_count = 0 139 | self.update_token_count_label() 140 | 141 | 142 | def mousePressEvent(self, event): 143 | if event.button() == Qt.MouseButton.LeftButton: 144 | self.oldPos = event.globalPosition().toPoint() 145 | self.resize_edge = self.get_resize_edge(event.pos()) 146 | if self.resize_edge: 147 | self.resizing = True 148 | else: 149 | self.resizing = False 150 | 151 | def mouseMoveEvent(self, event): 152 | if self.resizing: 153 | self.resize_window(event.globalPosition().toPoint()) 154 | elif event.buttons() & Qt.MouseButton.LeftButton: 155 | delta = event.globalPosition().toPoint() - self.oldPos 156 | self.move(self.x() + delta.x(), self.y() + delta.y()) 157 | 158 | self.update_cursor(event.pos()) 159 | self.oldPos = event.globalPosition().toPoint() 160 | 161 | def mouseReleaseEvent(self, event): 162 | self.resizing = False 163 | self.resize_edge = None 164 | self.unsetCursor() 165 | 166 | def get_resize_edge(self, pos): 167 | if 0 <= pos.x() <= self.resize_margin: 168 | if 0 <= pos.y() <= self.resize_margin: 169 | return 'top_left' 170 | elif self.height() - self.resize_margin <= pos.y() <= self.height(): 171 | return 'bottom_left' 172 | else: 173 | return 'left' 174 | return None 175 | 176 | def resize_window(self, global_pos): 177 | delta = global_pos - self.oldPos 178 | if self.resize_edge in ['left', 'top_left', 'bottom_left']: 179 | new_width = max(self.width() - delta.x(), 100) 180 | new_x = self.x() + self.width() - new_width 181 | self.setGeometry(new_x, self.y(), new_width, self.height()) 182 | if self.resize_edge in ['top_left']: 183 | new_height = max(self.height() - delta.y(), 100) 184 | new_y = self.y() + self.height() - new_height 185 | self.setGeometry(self.x(), new_y, self.width(), new_height) 186 | elif self.resize_edge in ['bottom_left']: 187 | new_height = max(self.height() + delta.y(), 100) 188 | self.resize(self.width(), new_height) 189 | 190 | def update_cursor(self, pos): 191 | resize_edge = self.get_resize_edge(pos) 192 | if resize_edge == 'left': 193 | self.setCursor(Qt.CursorShape.SizeHorCursor) 194 | elif resize_edge in ['top_left', 'bottom_left']: 195 | self.setCursor(Qt.CursorShape.SizeFDiagCursor) 196 | else: 197 | self.unsetCursor() 198 | 199 | def enterEvent(self, event): 200 | self.update_cursor(self.mapFromGlobal(self.cursor().pos())) 201 | 202 | def leaveEvent(self, event): 203 | self.unsetCursor() 204 | 205 | def update_token_count(self, message): 206 | # Simple word-based tokenization 207 | tokens = message.split() 208 | self.token_count += len(tokens) 209 | self.update_token_count_label() 210 | def update_token_count_label(self): 211 | self.token_count_label.setText(f"Tokens: {self.token_count}") -------------------------------------------------------------------------------- /langs_import.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | def list_files_with_extension(folder_path, file_extension): 4 | file_names = [] 5 | 6 | # Loop through all files in the folder 7 | for file_name in os.listdir(folder_path): 8 | # Check if the file ends with the specified extension 9 | if file_name.endswith(file_extension): 10 | # Add the file name without the extension to the list 11 | file_names.append(os.path.splitext(file_name)[0]) 12 | 13 | # Join the list of file names using the "+" symbol 14 | return "+".join(file_names) 15 | 16 | # Example usage 17 | folder_path = r'C:\Program Files\Tesseract-OCR\tessdata' # Update this to your folder path 18 | file_extension = '.traineddata' # Specify the extension you want to count (e.g., .txt, .py, .jpg) 19 | 20 | file_names_combined = list_files_with_extension(folder_path, file_extension) 21 | print(f"Files with '{file_extension}' extension: {file_names_combined}") 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | import os 4 | from PyQt6.QtWidgets import QApplication 5 | from chat_app import MinimalistChatApp 6 | 7 | # Get the home directory dynamically 8 | home_dir = os.path.expanduser("~") 9 | ollama_executable = os.path.join(home_dir, "AppData", "Local", "Programs", "Ollama", "ollama app.exe") 10 | 11 | # Use subprocess.Popen to run the executable without blocking 12 | subprocess.Popen([ollama_executable], shell=True) 13 | 14 | if __name__ == '__main__': 15 | app = QApplication(sys.argv) 16 | ex = MinimalistChatApp() 17 | ex.show() 18 | sys.exit(app.exec()) 19 | -------------------------------------------------------------------------------- /notes_window.py: -------------------------------------------------------------------------------- 1 | from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QLabel, QPushButton, QFileDialog, QMessageBox, QHBoxLayout 2 | from PyQt6.QtCore import Qt 3 | 4 | class NotesWindow(QWidget): 5 | def __init__(self, parent=None): 6 | super().__init__(parent, Qt.WindowType.Window | Qt.WindowType.FramelessWindowHint) 7 | self.initUI() 8 | self.oldPos = self.pos() 9 | self.resizing = True 10 | self.resize_edge = None 11 | self.resize_margin = 10 # The width of the "resizable" border area 12 | 13 | def initUI(self): 14 | self.setGeometry(1620, 390, 300, 600) 15 | self.setWindowTitle('Notes') 16 | 17 | # Create layout for the note-taking area 18 | layout = QVBoxLayout(self) 19 | 20 | # Add a QLabel for the title or instructions 21 | self.title_label = QLabel("Notes", self) 22 | self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Center the text 23 | 24 | # Add a QTextEdit for writing notes 25 | self.text_edit = QTextEdit(self) 26 | self.text_edit.setPlaceholderText("Write your notes here...") # Placeholder text 27 | 28 | # Add a horizontal layout for buttons 29 | button_layout = QHBoxLayout() 30 | 31 | # Add a Save button 32 | self.save_button = QPushButton("Save Notes", self) 33 | self.save_button.clicked.connect(self.save_notes_to_file) 34 | 35 | # Add an Open button 36 | self.open_button = QPushButton("Open Notes", self) 37 | self.open_button.clicked.connect(self.open_notes_from_file) 38 | 39 | self.clear_button = QPushButton("Clear", self) 40 | self.clear_button.clicked.connect(self.clear_notes) 41 | 42 | # Add buttons to button layout 43 | button_layout.addWidget(self.save_button) 44 | button_layout.addWidget(self.open_button) 45 | button_layout.addWidget(self.clear_button) 46 | 47 | 48 | # Add widgets to layout 49 | layout.addWidget(self.title_label) 50 | layout.addWidget(self.text_edit) 51 | layout.addLayout(button_layout) 52 | 53 | self.setLayout(layout) 54 | 55 | def clear_notes(self): 56 | self.text_edit.clear() 57 | self.title_label.setText(f"Notes") 58 | 59 | def save_notes_to_file(self): 60 | # Open a file dialog to save the file 61 | file_name, _ = QFileDialog.getSaveFileName(self, "Save Notes", "", "Text Files (*.txt);;All Files (*)") 62 | 63 | if file_name: # If a valid file name was chosen 64 | try: 65 | with open(file_name, 'w') as file: 66 | file.write(self.text_edit.toPlainText()) # Write the notes to the file 67 | # Show a message box indicating success 68 | QMessageBox.information(self, "Success", f"Notes saved to {file_name}") 69 | except Exception as e: 70 | # Show an error message if something goes wrong 71 | QMessageBox.critical(self, "Error", f"Failed to save notes: {str(e)}") 72 | 73 | def open_notes_from_file(self): 74 | # Open a file dialog to select a file to open 75 | file_name, _ = QFileDialog.getOpenFileName(self, "Open Notes", "", "Text Files (*.txt);;All Files (*)") 76 | 77 | if file_name: # If a valid file name was selected 78 | try: 79 | with open(file_name, 'r') as file: 80 | content = file.read() # Read the file's content 81 | self.text_edit.setPlainText(content) # Set the content to the QTextEdit 82 | 83 | # Extract just the file name (without the full path) 84 | file_display_name = file_name.split('/')[-1] # For Linux/Mac paths 85 | if '\\' in file_name: # For Windows paths 86 | file_display_name = file_name.split('\\')[-1] 87 | 88 | # Update the QLabel to display the file name 89 | self.title_label.setText(f"Notes - {file_display_name}") 90 | 91 | # Show a message box indicating success 92 | QMessageBox.information(self, "Success", f"Notes loaded from {file_name}") 93 | except Exception as e: 94 | # Show an error message if something goes wrong 95 | QMessageBox.critical(self, "Error", f"Failed to open file: {str(e)}") 96 | 97 | 98 | def mousePressEvent(self, event): 99 | if event.button() == Qt.MouseButton.LeftButton: 100 | self.oldPos = event.globalPosition().toPoint() 101 | self.resize_edge = self.get_resize_edge(event.pos()) 102 | if self.resize_edge: 103 | self.resizing = True 104 | else: 105 | self.resizing = False 106 | 107 | def mouseMoveEvent(self, event): 108 | if self.resizing: 109 | self.resize_window(event.globalPosition().toPoint()) 110 | elif event.buttons() & Qt.MouseButton.LeftButton: 111 | delta = event.globalPosition().toPoint() - self.oldPos 112 | self.move(self.x() + delta.x(), self.y() + delta.y()) 113 | 114 | self.update_cursor(event.pos()) 115 | self.oldPos = event.globalPosition().toPoint() 116 | 117 | def mouseReleaseEvent(self, event): 118 | self.resizing = False 119 | self.resize_edge = None 120 | self.unsetCursor() 121 | 122 | def get_resize_edge(self, pos): 123 | if 0 <= pos.x() <= self.resize_margin: 124 | if 0 <= pos.y() <= self.resize_margin: 125 | return 'top_left' 126 | elif self.height() - self.resize_margin <= pos.y() <= self.height(): 127 | return 'bottom_left' 128 | else: 129 | return 'left' 130 | return None 131 | 132 | def resize_window(self, global_pos): 133 | delta = global_pos - self.oldPos 134 | if self.resize_edge in ['left', 'top_left', 'bottom_left']: 135 | new_width = max(self.width() - delta.x(), 100) 136 | new_x = self.x() + self.width() - new_width 137 | self.setGeometry(new_x, self.y(), new_width, self.height()) 138 | if self.resize_edge in ['top_left']: 139 | new_height = max(self.height() - delta.y(), 100) 140 | new_y = self.y() + self.height() - new_height 141 | self.setGeometry(self.x(), new_y, self.width(), new_height) 142 | elif self.resize_edge in ['bottom_left']: 143 | new_height = max(self.height() + delta.y(), 100) 144 | self.resize(self.width(), new_height) 145 | 146 | def update_cursor(self, pos): 147 | resize_edge = self.get_resize_edge(pos) 148 | if resize_edge == 'left': 149 | self.setCursor(Qt.CursorShape.SizeHorCursor) 150 | elif resize_edge in ['top_left', 'bottom_left']: 151 | self.setCursor(Qt.CursorShape.SizeFDiagCursor) 152 | else: 153 | self.unsetCursor() 154 | 155 | def enterEvent(self, event): 156 | self.update_cursor(self.mapFromGlobal(self.cursor().pos())) 157 | 158 | def leaveEvent(self, event): 159 | self.unsetCursor() 160 | -------------------------------------------------------------------------------- /ocr.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tkinter as tk 3 | from tkinter import ttk, Tk, Label, Text, Scrollbar, VERTICAL, RIGHT, Y, Frame, Toplevel, Canvas 4 | from PIL import Image, ImageTk 5 | import mss 6 | import mss.tools 7 | import pytesseract 8 | import itertools 9 | import threading 10 | from langdetect import detect 11 | pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' 12 | tess_map = { 13 | "afr": "Afrikaans", "amh": "Amharic", "ara": "Arabic", "asm": "Assamese", 14 | "aze": "Azerbaijani", "aze_cyrl": "Azerbaijani (Cyrillic)", 15 | "bel": "Belarusian", "ben": "Bengali", "bod": "Tibetan", "bos": "Bosnian", 16 | "bre": "Breton", "bul": "Bulgarian", "cat": "Catalan", "ceb": "Cebuano", 17 | "ces": "Czech", "chi_sim": "Chinese (Simplified)", 18 | "chi_sim_vert": "Chinese (Simplified, Vertical)", 19 | "chi_tra": "Chinese (Traditional)", "chi_tra_vert": "Chinese (Traditional, Vertical)", 20 | "chr": "Cherokee", "cos": "Corsican", "cym": "Welsh", "dan": "Danish", 21 | "dan_frak": "Danish (Fraktur)", "deu": "German", "deu_frak": "German (Fraktur)", 22 | "deu_latf": "German (Latin, Fraktur)", "div": "Dhivehi", 23 | "dzo": "Dzongkha", "ell": "Greek", "eng": "English", "enm": "Middle English", 24 | "epo": "Esperanto", "equ": "Math/Symbol", "est": "Estonian", "eus": "Basque", 25 | "fao": "Faroese", "fas": "Persian", "fil": "Filipino", "fin": "Finnish", 26 | "fra": "French", "frm": "Middle French", "fry": "Frisian", "gla": "Scottish Gaelic", 27 | "gle": "Irish", "glg": "Galician", "grc": "Ancient Greek", "guj": "Gujarati", 28 | "hat": "Haitian", "heb": "Hebrew", "hin": "Hindi", "hrv": "Croatian", 29 | "hun": "Hungarian", "hye": "Armenian", "iku": "Inuktitut", "ind": "Indonesian", 30 | "isl": "Icelandic", "ita": "Italian", "ita_old": "Old Italian", 31 | "jav": "Javanese", "jpn": "Japanese", "jpn_vert": "Japanese (Vertical)", 32 | "kan": "Kannada", "kat": "Georgian", "kat_old": "Old Georgian", 33 | "kaz": "Kazakh", "khm": "Khmer", "kir": "Kyrgyz", "kmr": "Kurdish", 34 | "kor": "Korean", "kor_vert": "Korean (Vertical)", 35 | "lao": "Lao", "lat": "Latin", "lav": "Latvian", "lit": "Lithuanian", 36 | "ltz": "Luxembourgish", "mal": "Malayalam", "mar": "Marathi", "mkd": "Macedonian", 37 | "mlt": "Maltese", "mon": "Mongolian", "mri": "Maori", "msa": "Malay", 38 | "mya": "Burmese", "nep": "Nepali", "nld": "Dutch", "nor": "Norwegian", 39 | "oci": "Occitan", "ori": "Oriya", "osd": "Orientation Script Detection", 40 | "pan": "Punjabi", "pol": "Polish", "por": "Portuguese", 41 | "pus": "Pashto", "que": "Quechua", "ron": "Romanian", "rus": "Russian", 42 | "san": "Sanskrit", "sin": "Sinhala", "slk": "Slovak", "slk_frak": "Slovak (Fraktur)", 43 | "slv": "Slovenian", "snd": "Sindhi", "spa": "Spanish", "spa_old": "Old Spanish", 44 | "sqi": "Albanian", "srp": "Serbian", "srp_latn": "Serbian (Latin)", 45 | "sun": "Sundanese", "swa": "Swahili", "swe": "Swedish", 46 | "syr": "Syriac", "tam": "Tamil", "tat": "Tatar", "tel": "Telugu", 47 | "tgk": "Tajik", "tgl": "Tagalog", "tha": "Thai", "tir": "Tigrinya", 48 | "ton": "Tongan", "tur": "Turkish", "uig": "Uyghur", "ukr": "Ukrainian", 49 | "urd": "Urdu", "uzb": "Uzbek", "uzb_cyrl": "Uzbek (Cyrillic)", 50 | "vie": "Vietnamese", "yid": "Yiddish", "yor": "Yoruba" 51 | } 52 | def set_dark_theme(root): 53 | style = ttk.Style(root) 54 | style.theme_create("dark_theme", parent="alt", settings={ 55 | "TFrame": {"configure": {"background": "#1e1e1e"}}, 56 | "TLabel": {"configure": {"background": "#1e1e1e", "foreground": "#dcdcdc"}}, 57 | "TButton": {"configure": {"background": "#3c3c3c", "foreground": "#dcdcdc"}}, 58 | "TEntry": {"configure": {"fieldbackground": "#0f0f0f", "foreground": "#dcdcdc"}}, 59 | "TText": {"configure": {"background": "#0f0f0f", "foreground": "#dcdcdc"}}, 60 | "TCombobox": {"configure": {"fieldbackground": "#0f0f0f", "foreground": "#dcdcdc"}}, 61 | "Vertical.TScrollbar": {"configure": {"background": "#3c3c3c", "troughcolor": "#1e1e1e"}}, 62 | }) 63 | style.theme_use("dark_theme") 64 | 65 | # Configure colors for standard Tkinter widgets 66 | root.configure(bg="#1e1e1e") 67 | root.option_add("*Background", "#1e1e1e") 68 | root.option_add("*Foreground", "#dcdcdc") 69 | root.option_add("*Entry.Background", "#0f0f0f") 70 | root.option_add("*Entry.Foreground", "#dcdcdc") 71 | root.option_add("*Text.Background", "#0f0f0f") 72 | root.option_add("*Text.Foreground", "#dcdcdc") 73 | class ScrollableFrame(ttk.Frame): 74 | def __init__(self, container, *args, **kwargs): 75 | super().__init__(container, *args, **kwargs) 76 | self.canvas = tk.Canvas(self) 77 | scrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview) 78 | self.scrollable_frame = ttk.Frame(self.canvas) 79 | 80 | self.scrollable_frame.bind( 81 | "", 82 | lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")) 83 | ) 84 | 85 | self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") 86 | self.canvas.configure(yscrollcommand=scrollbar.set) 87 | 88 | self.canvas.pack(side="left", fill="both", expand=True) 89 | scrollbar.pack(side="right", fill="y") 90 | 91 | # Bind mousewheel to scrolling 92 | self.bind_mousewheel(self.canvas) 93 | self.bind_mousewheel(self.scrollable_frame) 94 | 95 | def bind_mousewheel(self, widget): 96 | widget.bind("", self._on_mousewheel) 97 | widget.bind("", self._on_mousewheel) # For Linux 98 | widget.bind("", self._on_mousewheel) # For Linux 99 | 100 | def _on_mousewheel(self, event): 101 | if event.num == 5 or event.delta == -120: # Scroll down 102 | self.canvas.yview_scroll(1, "units") 103 | elif event.num == 4 or event.delta == 120: # Scroll up 104 | self.canvas.yview_scroll(-1, "units") 105 | class AnimatedLabel(ttk.Label): 106 | def __init__(self, master, **kwargs): 107 | super().__init__(master, **kwargs) 108 | self._is_running = False 109 | self._spinner = itertools.cycle(['-', '/', '|', '\\']) 110 | self._animation_id = None 111 | 112 | def start(self): 113 | """Start the spinner animation.""" 114 | if not self._is_running: 115 | self._is_running = True 116 | self._animate() 117 | 118 | def stop(self): 119 | """Stop the spinner animation.""" 120 | self._is_running = False 121 | if self._animation_id: 122 | self.after_cancel(self._animation_id) # Cancel the scheduled `after` call 123 | self._animation_id = None 124 | self.configure(text="") # Clear the label 125 | 126 | def _animate(self): 127 | """Animate the spinner by updating the text periodically using after().""" 128 | if self._is_running: 129 | self.configure(text=next(self._spinner) + " Processing...") 130 | self._animation_id = self.after(100, self._animate) 131 | 132 | class OCR: 133 | def __init__(self, root): 134 | self.root = root 135 | self.window = tk.Toplevel(root) 136 | self.window.title("OCR Window") 137 | set_dark_theme(self.window) 138 | 139 | width = 400 140 | height = 600 141 | x_offset = 1500 142 | y_offset = 50 143 | 144 | # Set the geometry of the window: "widthxheight+x_offset+y_offset" 145 | self.window.geometry(f"{width}x{height}+{x_offset}+{y_offset}") 146 | 147 | # Create a scrollable frame 148 | self.scroll_frame = ScrollableFrame(self.window) 149 | self.scroll_frame.pack(fill="both", expand=True) 150 | 151 | # Add the animated label 152 | self.loading_label = AnimatedLabel(self.scroll_frame.scrollable_frame) 153 | self.loading_label.pack(padx=10, pady=10) 154 | 155 | # Add the Select Area button to the scrollable frame 156 | self.select_button = ttk.Button(self.scroll_frame.scrollable_frame, text="Select Area", command=self.select_area) 157 | self.select_button.pack(padx=10, pady=10) 158 | 159 | # Create a dropdown menu (combobox) under the Select Area button 160 | # Set the StringVar for the dropdown 161 | self.dropdown_var = tk.StringVar() 162 | 163 | # Create the Combobox with sorted language values 164 | self.dropdown = ttk.Combobox(self.scroll_frame.scrollable_frame, textvariable=self.dropdown_var, state='readonly') 165 | self.dropdown['values'] = sorted(tess_map.values()) 166 | 167 | # Set "English" as the default selection 168 | self.dropdown_var.set("English") # Use the name directly 169 | 170 | # Ensure the Combobox reflects the default selection 171 | self.dropdown.pack(padx=10, pady=10) 172 | 173 | self.result_label = ttk.Label(self.scroll_frame.scrollable_frame, text="") 174 | self.result_label.pack(padx=10, pady=10) 175 | 176 | self.lang_label = ttk.Label(self.scroll_frame.scrollable_frame, text="") 177 | self.lang_label.pack(padx=10, pady=10) 178 | 179 | # Create an image display area in the OCR window 180 | self.image_label = tk.Label(self.scroll_frame.scrollable_frame) 181 | self.image_label.pack(padx=10, pady=10) 182 | 183 | # Create a text field for OCR output 184 | self.text_output = tk.Text(self.scroll_frame.scrollable_frame, height=10, width=50) 185 | self.text_output.pack(padx=10, pady=10) 186 | 187 | self.button_frame = ttk.Frame(self.scroll_frame.scrollable_frame) 188 | self.button_frame.pack(padx=10, pady=10) 189 | 190 | # Create an OCR button 191 | self.ocr_button = ttk.Button(self.button_frame, text="Perform OCR", command=self.perform_ocr) 192 | self.ocr_button.pack(side=tk.LEFT, padx=(0, 5)) 193 | 194 | # Create a Clear button 195 | self.clear_button = ttk.Button(self.button_frame, text="Clear", command=self.clear_ocr) 196 | self.clear_button.pack(side=tk.LEFT, padx=(5, 0)) 197 | 198 | self.lang_button = ttk.Button(self.button_frame, text="Language Detection", command=self.lang_detect) 199 | self.lang_button.pack(side=tk.LEFT, padx=(5, 0)) 200 | 201 | 202 | 203 | def select_area(self): 204 | self.window.withdraw() # Hide the OCR window 205 | self.root.withdraw() # Hide the main window 206 | self.screenshot = mss.mss() 207 | self.root.after(100, self.start_selection) 208 | 209 | def start_selection(self): 210 | self.select_window = tk.Toplevel(self.root) 211 | self.select_window.attributes('-fullscreen', True) 212 | self.select_window.attributes('-alpha', 0.3) 213 | self.select_window.configure(cursor="cross") 214 | 215 | self.canvas = tk.Canvas(self.select_window, highlightthickness=0) 216 | self.canvas.pack(fill=tk.BOTH, expand=True) 217 | 218 | self.select_window.bind("", self.on_press) 219 | self.select_window.bind("", self.on_drag) 220 | self.select_window.bind("", self.on_release) 221 | 222 | self.rect = None 223 | self.start_x = None 224 | self.start_y = None 225 | 226 | def on_press(self, event): 227 | self.start_x = self.select_window.winfo_pointerx() 228 | self.start_y = self.select_window.winfo_pointery() 229 | self.rect = self.canvas.create_rectangle(self.start_x, self.start_y, self.start_x, self.start_y, outline='red') 230 | 231 | def on_drag(self, event): 232 | cur_x = self.select_window.winfo_pointerx() 233 | cur_y = self.select_window.winfo_pointery() 234 | self.canvas.coords(self.rect, self.start_x, self.start_y, cur_x, cur_y) 235 | 236 | def on_release(self, event): 237 | end_x = self.select_window.winfo_pointerx() 238 | end_y = self.select_window.winfo_pointery() 239 | self.select_window.destroy() 240 | self.window.deiconify() # Show the OCR window again 241 | 242 | left = min(self.start_x, end_x) 243 | top = min(self.start_y, end_y) 244 | width = abs(end_x - self.start_x) 245 | height = abs(end_y - self.start_y) 246 | 247 | monitor = {"top": top, "left": left, "width": width, "height": height} 248 | screenshot = self.screenshot.grab(monitor) 249 | mss.tools.to_png(screenshot.rgb, screenshot.size, output="./assets/screenshot.png") 250 | 251 | self.result_label.config(text="Screenshot captured") 252 | self.display_image("./assets/screenshot.png") 253 | 254 | def display_image(self, image_path): 255 | # Open the image file 256 | img = Image.open(image_path) 257 | 258 | # Resize the image to fit within a maximum size (e.g., 400x400) 259 | img.thumbnail((400, 400)) 260 | 261 | # Convert the Image object to a PhotoImage object 262 | photo = ImageTk.PhotoImage(img) 263 | 264 | # Update the image in the label 265 | self.image_label.config(image=photo) 266 | self.image_label.image = photo # Keep a reference to avoid garbage collection 267 | 268 | def perform_ocr(self): 269 | try: 270 | # Get the selected option's full name 271 | selected_option = self.dropdown_var.get() 272 | # Get the corresponding code from tess_map 273 | selected_code = [code for code, name in tess_map.items() if name == selected_option][0] 274 | 275 | # Perform OCR using the selected language code 276 | text = pytesseract.image_to_string(Image.open("./assets/screenshot.png"), lang=selected_code) 277 | 278 | self.text_output.delete(1.0, tk.END) # Clear previous text 279 | self.text_output.insert(tk.END, text) 280 | self.result_label.config(text="OCR completed") 281 | with open("./assets/ocr_output.txt", "w", encoding="utf-8") as f: 282 | f.write(text) 283 | except Exception as e: 284 | self.result_label.config(text=f"Error: {str(e)}") 285 | 286 | def lang_detect(self): 287 | self.loading_label.start() 288 | self.lang_button.config(state=tk.DISABLED) 289 | def run_ocr(): 290 | detect_map = { 291 | 'af': 'Afrikaans', 'ar': 'Arabic', 'bg': 'Bulgarian', 'bn': 'Bengali', 292 | 'ca': 'Catalan', 'cs': 'Czech', 'cy': 'Welsh', 'da': 'Danish', 293 | 'de': 'German', 'el': 'Greek', 'en': 'English', 'es': 'Spanish', 294 | 'et': 'Estonian', 'fa': 'Persian', 'fi': 'Finnish', 'fr': 'French', 295 | 'gu': 'Gujarati', 'he': 'Hebrew', 'hi': 'Hindi', 'hr': 'Croatian', 296 | 'hu': 'Hungarian', 'id': 'Indonesian', 'it': 'Italian', 'ja': 'Japanese', 297 | 'kn': 'Kannada', 'ko': 'Korean', 'lt': 'Lithuanian', 'lv': 'Latvian', 298 | 'mk': 'Macedonian', 'ml': 'Malayalam', 'mr': 'Marathi', 'ne': 'Nepali', 299 | 'nl': 'Dutch', 'no': 'Norwegian', 'pa': 'Punjabi', 'pl': 'Polish', 300 | 'pt': 'Portuguese', 'ro': 'Romanian', 'ru': 'Russian', 'sk': 'Slovak', 301 | 'sl': 'Slovenian', 'so': 'Somali', 'sq': 'Albanian', 'sv': 'Swedish', 302 | 'sw': 'Swahili', 'ta': 'Tamil', 'te': 'Telugu', 'th': 'Thai', 303 | 'tl': 'Tagalog', 'tr': 'Turkish', 'uk': 'Ukrainian', 'ur': 'Urdu', 304 | 'vi': 'Vietnamese', 'zh-cn': 'Chinese (Simplified)', 'zh-tw': 'Chinese (Traditional)' 305 | } 306 | try: 307 | text = pytesseract.image_to_string(Image.open("./assets/screenshot.png"), lang='afr+amh+ara+asm+aze+aze_cyrl+bel+ben+bod+bos+bre+bul+cat+ceb+ces+chi_sim+chi_sim_vert+chi_tra+chi_tra_vert+chr+cos+cym+dan+dan_frak+deu+deu_frak+deu_latf+div+dzo+ell+eng+enm+epo+equ+est+eus+fao+fas+fil+fin+fra+frm+fry+gla+gle+glg+grc+guj+hat+heb+hin+hrv+hun+hye+iku+ind+isl+ita+ita_old+jav+jpn+jpn_vert+kan+kat+kat_old+kaz+khm+kir+kmr+kor+kor_vert+lao+lat+lav+lit+ltz+mal+mar+mkd+mlt+mon+mri+msa+mya+nep+nld+nor+oci+ori+osd+pan+pol+por+pus+que+ron+rus+san+sin+slk+slk_frak+slv+snd+spa+spa_old+sqi+srp+srp_latn+sun+swa+swe+syr+tam+tat+tel+tgk+tgl+tha+tir+ton+tur+uig+ukr+urd+uzb+uzb_cyrl+vie+yid+yor') 308 | lang = detect(text) 309 | full_lang_name = detect_map.get(lang, 'Unknown language') 310 | 311 | self.window.after(0, lambda: self._update_gui(text, full_lang_name)) 312 | 313 | with open("./assets/ocr_output.txt", "w", encoding="utf-8") as f: 314 | f.write(text) 315 | 316 | except Exception as e: 317 | self.window.after(0, lambda: self.lang_label.config(text=f"Language Detection Failed: {str(e)}")) 318 | finally: 319 | self.window.after(0, self._finish_processing) 320 | 321 | threading.Thread(target=run_ocr, daemon=True).start() 322 | 323 | def _update_gui(self, text, full_lang_name): 324 | self.window.after(0, lambda: self.text_output.delete(1.0, tk.END)) 325 | self.window.after(0, lambda: self.insert_text_in_chunks(text)) 326 | self.window.after(0, lambda: self.lang_label.config(text=f'Language detected as: {full_lang_name}')) 327 | 328 | def insert_text_in_chunks(self, text, index=0, chunk_size=100): 329 | if index < len(text): 330 | self.text_output.insert(tk.END, text[index:index + chunk_size]) 331 | self.window.after(1, self.insert_text_in_chunks, text, index + chunk_size) 332 | 333 | def _finish_processing(self): 334 | # Schedule the stop() call and button re-enabling to happen on the main thread 335 | if self.loading_label.winfo_exists(): 336 | # Ensure that stop() runs on the main thread using after 337 | self.window.after(0, lambda: self.loading_label.stop()) 338 | else: 339 | print("Warning: Loading label no longer exists.") 340 | 341 | # Re-enable the button safely on the main thread 342 | self.window.after(0, self.lang_button.config, {"state": tk.NORMAL}) 343 | 344 | def clear_ocr(self): 345 | try: 346 | # Delete the ocr_output.txt file 347 | if os.path.exists("./assets/ocr_output.txt"): 348 | os.remove("./assets/ocr_output.txt") 349 | self.result_label.config(text="OCR output cleared") 350 | else: 351 | self.result_label.config(text="No OCR output to clear") 352 | 353 | # Clear the text output field 354 | self.text_output.delete(1.0, tk.END) 355 | self.image_label.config(image='') # Clear the image in the label 356 | self.image_label.image = None # Remove the reference to the image 357 | self.lang_label.config(text='') 358 | except Exception as e: 359 | self.result_label.config(text=f"Clear failed: {str(e)}") 360 | 361 | def run(self): 362 | self.window.mainloop() -------------------------------------------------------------------------------- /terminal_monitor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import psutil 4 | import time 5 | import platform 6 | import GPUtil 7 | import datetime 8 | import socket 9 | from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, 10 | QTabWidget, QTextEdit, QLineEdit, QHBoxLayout, QGroupBox, 11 | QProgressBar, QFileDialog, QListView, QScrollArea, QFrame) 12 | from PyQt6.QtGui import QColor, QPalette, QFont, QTextCursor 13 | from PyQt6.QtCore import QTimer, Qt, pyqtSignal, QProcess, QStringListModel 14 | 15 | class NetworkWidget(QWidget): 16 | def __init__(self): 17 | super().__init__() 18 | self.initUI() 19 | self.updateNetworkStats() 20 | 21 | def initUI(self): 22 | # Set up the layout 23 | self.layout = QVBoxLayout() 24 | 25 | # Labels to show the hostname, IP, and network stats 26 | self.hostname_label = QLabel("Hostname: ") 27 | self.ip_address_label = QLabel("IP Address: ") 28 | self.network_stats_label = QLabel("Network Stats: ") 29 | 30 | # Add the labels to the main layout 31 | self.layout.addWidget(self.hostname_label) 32 | self.layout.addWidget(self.ip_address_label) 33 | self.layout.addWidget(self.network_stats_label) 34 | 35 | # Create two scroll areas, one for Open Ports and one for In Use Ports 36 | self.open_ports_scroll_area = QScrollArea() 37 | self.open_ports_scroll_area.setWidgetResizable(True) 38 | 39 | self.in_use_ports_scroll_area = QScrollArea() 40 | self.in_use_ports_scroll_area.setWidgetResizable(True) 41 | 42 | # Create containers for both open and in-use ports 43 | self.open_ports_container = QFrame() 44 | self.open_ports_layout = QVBoxLayout() 45 | self.open_ports_container.setLayout(self.open_ports_layout) 46 | 47 | self.in_use_ports_container = QFrame() 48 | self.in_use_ports_layout = QVBoxLayout() 49 | self.in_use_ports_container.setLayout(self.in_use_ports_layout) 50 | 51 | # Set widgets for scroll areas 52 | self.open_ports_scroll_area.setWidget(self.open_ports_container) 53 | self.in_use_ports_scroll_area.setWidget(self.in_use_ports_container) 54 | 55 | # Create a layout for the labels (Open Ports and In Use Ports) 56 | labels_layout = QHBoxLayout() 57 | open_ports_label = QLabel("Open Ports:") 58 | in_use_ports_label = QLabel("In Use Ports:") 59 | 60 | labels_layout.addWidget(open_ports_label) 61 | labels_layout.addWidget(in_use_ports_label) 62 | 63 | # Align the "In Use Ports" label to the right 64 | in_use_ports_label.setAlignment(Qt.AlignmentFlag.AlignRight) 65 | 66 | # Use QHBoxLayout to display the two scroll areas side by side 67 | ports_layout = QHBoxLayout() 68 | ports_layout.addWidget(self.open_ports_scroll_area) 69 | ports_layout.addWidget(self.in_use_ports_scroll_area) 70 | 71 | # Add the labels and the scrollable areas into the main layout 72 | self.layout.addLayout(labels_layout) # Add the labels for the columns 73 | self.layout.addLayout(ports_layout) # Add the scrollable areas 74 | 75 | # Set the layout for the widget 76 | self.setLayout(self.layout) 77 | 78 | # Set up a timer to periodically update network metrics 79 | self.timer = QTimer() 80 | self.timer.timeout.connect(self.updateNetworkStats) 81 | self.timer.start(5000) # Update every 5 seconds 82 | 83 | def updateNetworkStats(self): 84 | # Get and display the hostname 85 | hostname = socket.gethostname() 86 | self.hostname_label.setText(f"Hostname: {hostname}") 87 | 88 | # Get and display the IP address 89 | ip_address = self.get_ip_address() 90 | self.ip_address_label.setText(f"IP Address: {ip_address}") 91 | 92 | # Get and display network stats 93 | network_stats = self.get_network_stats() 94 | self.network_stats_label.setText(f"Network Stats: {network_stats}") 95 | 96 | # Update the open and in-use ports 97 | self.update_ports() 98 | 99 | def update_ports(self): 100 | """Update the lists of open and in-use ports.""" 101 | # Clear the current contents of the layouts 102 | self.clear_layout(self.open_ports_layout) 103 | self.clear_layout(self.in_use_ports_layout) 104 | 105 | # Get the open and in-use ports 106 | open_ports, in_use_ports = self.get_ports() 107 | 108 | # Display each open port in the open ports scroll area 109 | for port in open_ports: 110 | open_port_label = QLabel(f"Port: {port}") 111 | self.open_ports_layout.addWidget(open_port_label) 112 | 113 | # Display each in-use port in the in-use ports scroll area 114 | for port in in_use_ports: 115 | in_use_port_label = QLabel(f"Port: {port}") 116 | self.in_use_ports_layout.addWidget(in_use_port_label) 117 | 118 | def clear_layout(self, layout): 119 | """Helper function to clear the contents of a layout.""" 120 | for i in reversed(range(layout.count())): 121 | widget_to_remove = layout.itemAt(i).widget() 122 | layout.removeWidget(widget_to_remove) 123 | widget_to_remove.deleteLater() 124 | 125 | def get_ip_address(self): 126 | """Get the IP address of the machine.""" 127 | try: 128 | ip_address = socket.gethostbyname(socket.gethostname()) 129 | return ip_address 130 | except socket.error as e: 131 | return f"Error: {str(e)}" 132 | 133 | def get_ports(self): 134 | """Get lists of open and in-use ports.""" 135 | connections = psutil.net_connections() 136 | 137 | # Open ports (ports in 'LISTEN' state) 138 | open_ports = [conn.laddr.port for conn in connections if conn.status == 'LISTEN'] 139 | 140 | # In-use ports (ports in 'ESTABLISHED' state) 141 | in_use_ports = [conn.laddr.port for conn in connections if conn.status == 'ESTABLISHED'] 142 | 143 | return open_ports, in_use_ports 144 | 145 | def get_network_stats(self): 146 | """Get some basic network stats.""" 147 | stats = psutil.net_if_stats() 148 | network_info = [] 149 | for iface, stat in stats.items(): 150 | if stat.isup: 151 | network_info.append(f"{iface}: {'UP' if stat.isup else 'DOWN'}") 152 | return ', '.join(network_info) 153 | 154 | 155 | class DiskUsageWidget(QWidget): 156 | def __init__(self, parent=None): 157 | super().__init__(parent) 158 | self.layout = QVBoxLayout(self) 159 | self.bars = {} 160 | 161 | def update_usage(self): 162 | # Fetch the list of partitions 163 | partitions = psutil.disk_partitions() 164 | 165 | # Create or update progress bars for each partition 166 | for partition in partitions: 167 | try: 168 | # Skip drives that are not ready (e.g., CD drives or unmounted drives) 169 | if 'cdrom' in partition.opts or not partition.fstype: 170 | continue 171 | 172 | # Attempt to get disk usage for the partition 173 | usage = psutil.disk_usage(partition.mountpoint) 174 | 175 | # Check if this partition's bar already exists, otherwise create it 176 | if partition.device not in self.bars: 177 | bar = QProgressBar(self) 178 | bar.setRange(0, 100) 179 | self.layout.addWidget(bar) 180 | self.bars[partition.device] = bar 181 | 182 | # Update the progress bar's value and label 183 | bar = self.bars[partition.device] 184 | bar.setValue(int(usage.percent)) # Cast to int for QProgressBar 185 | 186 | # Format the label with drive, used space, total space, and percentage 187 | used_gb = usage.used / (1024 ** 3) 188 | total_gb = usage.total / (1024 ** 3) 189 | bar.setFormat(f"{partition.device}: {used_gb:.2f} GB / {total_gb:.2f} GB ({int(usage.percent)}%)") 190 | 191 | except PermissionError: 192 | print(f"Permission denied for {partition.device}. Skipping.") 193 | continue 194 | except OSError as e: 195 | if e.winerror == 21: # WinError 21: The device is not ready 196 | print(f"Drive {partition.device} is not ready. Skipping.") 197 | else: 198 | print(f"Error for {partition.device}: {e}") 199 | continue 200 | 201 | # Remove bars that are no longer relevant (e.g., if a device was disconnected) 202 | for device in list(self.bars.keys()): 203 | if device not in [partition.device for partition in partitions]: 204 | bar = self.bars.pop(device) 205 | self.layout.removeWidget(bar) 206 | bar.deleteLater() 207 | 208 | 209 | 210 | 211 | class PCMonitorWidget(QWidget): 212 | def __init__(self, parent=None): 213 | super().__init__(parent) 214 | self.initUI() 215 | 216 | def initUI(self): 217 | layout = QHBoxLayout(self) 218 | 219 | # Left column - Dynamic metrics 220 | left_group = QGroupBox("System Metrics") 221 | left_layout = QVBoxLayout(left_group) 222 | 223 | self.cpu_label = QLabel("CPU Usage: ") 224 | self.memory_label = QLabel("Memory Usage: ") 225 | self.network_label = QLabel("Network Usage: ") 226 | self.uptime_label = QLabel("System Uptime: ") 227 | self.boot_time_label = QLabel("Boot Time: ") # Initialize boot_time_label 228 | 229 | left_layout.addWidget(self.cpu_label) 230 | left_layout.addWidget(self.memory_label) 231 | left_layout.addWidget(self.network_label) 232 | left_layout.addWidget(self.uptime_label) 233 | left_layout.addWidget(self.boot_time_label) # Add boot_time_label to the layout 234 | 235 | # Add disk usage widget 236 | self.disk_usage_widget = DiskUsageWidget() 237 | left_layout.addWidget(QLabel("Storage Usage:")) 238 | left_layout.addWidget(self.disk_usage_widget) 239 | 240 | left_layout.addStretch() 241 | 242 | # Right column - Hardware specs 243 | right_group = QGroupBox("Hardware Specifications") 244 | right_layout = QVBoxLayout(right_group) 245 | 246 | self.cpu_info = QLabel("CPU: ") 247 | self.gpu_info = QLabel("GPU: ") 248 | self.ram_info = QLabel("RAM: ") 249 | 250 | right_layout.addWidget(self.cpu_info) 251 | right_layout.addWidget(self.gpu_info) 252 | right_layout.addWidget(self.ram_info) 253 | right_layout.addStretch() 254 | 255 | layout.addWidget(left_group) 256 | layout.addWidget(right_group) 257 | 258 | self.last_net_io = psutil.net_io_counters() 259 | self.last_net_time = time.time() 260 | 261 | # Initialize hardware specs 262 | self.update_hardware_specs() 263 | 264 | def update_hardware_specs(self): 265 | # CPU Info 266 | cpu_info = platform.processor() 267 | self.cpu_info.setText(f"CPU: {cpu_info}") 268 | 269 | # GPU Info 270 | try: 271 | gpus = GPUtil.getGPUs() 272 | gpu_info = gpus[0].name if gpus else "N/A" 273 | except: 274 | gpu_info = "Unable to retrieve GPU info" 275 | self.gpu_info.setText(f"GPU: {gpu_info}") 276 | 277 | # RAM Info 278 | ram = psutil.virtual_memory() 279 | ram_total = ram.total / (1024 ** 3) # Convert to GB 280 | self.ram_info.setText(f"RAM: {ram_total:.2f} GB") 281 | 282 | def update_stats(self): 283 | # CPU Usage 284 | cpu_percent = psutil.cpu_percent() 285 | self.cpu_label.setText(f"CPU Usage: {cpu_percent:.1f}%") 286 | 287 | # Memory Usage 288 | memory = psutil.virtual_memory() 289 | self.memory_label.setText(f"Memory Usage: {memory.percent:.1f}%") 290 | 291 | # Network Usage 292 | current_net_io = psutil.net_io_counters() 293 | current_net_time = time.time() 294 | 295 | duration = current_net_time - self.last_net_time 296 | bytes_sent = (current_net_io.bytes_sent - self.last_net_io.bytes_sent) / duration 297 | bytes_recv = (current_net_io.bytes_recv - self.last_net_io.bytes_recv) / duration 298 | 299 | self.network_label.setText(f"Network: ↑ {bytes_sent / 1024:.1f} KB/s, ↓ {bytes_recv / 1024:.1f} KB/s") 300 | 301 | self.last_net_io = current_net_io 302 | self.last_net_time = current_net_time 303 | 304 | # System Uptime 305 | uptime = int(time.time() - psutil.boot_time()) 306 | # Convert uptime to days, hours, minutes, seconds 307 | days, remainder = divmod(uptime, 86400) # 86400 seconds in a day 308 | hours, remainder = divmod(remainder, 3600) # 3600 seconds in an hour 309 | minutes, seconds = divmod(remainder, 60) # 60 seconds in a minute 310 | 311 | # Update the uptime label 312 | self.uptime_label.setText(f"System Uptime: {days}d {hours}h {minutes}m {seconds}s") 313 | 314 | # Get the boot time and convert it to local datetime 315 | boot_time = psutil.boot_time() 316 | boot_time_dt = datetime.datetime.fromtimestamp(boot_time) # Convert to local time 317 | 318 | # Format the boot time for display 319 | boot_time_str = boot_time_dt.strftime("%Y-%m-%d %H:%M:%S") # Example format: "2024-09-26 15:30:00" 320 | 321 | # Update the boot time label 322 | self.boot_time_label.setText(f"Boot Time: {boot_time_str}") 323 | 324 | # Update Disk Usage 325 | self.disk_usage_widget.update_usage() 326 | 327 | 328 | 329 | 330 | 331 | class TerminalWidget(QWidget): 332 | command_executed = pyqtSignal(str, str) # Signal for command output (stdout, stderr) 333 | 334 | def __init__(self, parent=None): 335 | super().__init__(parent) 336 | self.process = QProcess(self) 337 | self.current_path = os.getcwd() # Initialize current working directory 338 | self.command_history = [] 339 | self.history_index = -1 # Track history index 340 | self.initUI() 341 | 342 | def initUI(self): 343 | layout = QVBoxLayout(self) 344 | 345 | # Output text area 346 | self.output_text = QTextEdit(self) 347 | self.output_text.setReadOnly(True) 348 | self.output_text.setFont(QFont("Consolas", 10)) 349 | layout.addWidget(self.output_text) 350 | 351 | # Input line 352 | self.input_line = QLineEdit(self) 353 | self.input_line.setFont(QFont("Consolas", 10)) 354 | self.input_line.returnPressed.connect(self.execute_command) 355 | layout.addWidget(self.input_line) 356 | 357 | # File list view 358 | self.file_view = QListView(self) 359 | self.file_view.setFixedHeight(200) # Set a fixed height for the file view 360 | layout.addWidget(self.file_view) 361 | 362 | # Set up QProcess 363 | self.process.readyReadStandardOutput.connect(self.handle_stdout) 364 | self.process.readyReadStandardError.connect(self.handle_stderr) 365 | self.process.finished.connect(self.process_finished) 366 | 367 | # Initial output 368 | self.output_text.append("Windows Terminal") 369 | self.update_file_list() # Initialize the file list 370 | self.show_prompt() # Show initial prompt 371 | 372 | def show_prompt(self): 373 | self.output_text.append(f"\n{self.current_path}> ") 374 | self.output_text.moveCursor(QTextCursor.MoveOperation.End) 375 | 376 | def execute_command(self): 377 | command = self.input_line.text().strip() 378 | if command: 379 | self.command_history.append(command) 380 | self.history_index = len(self.command_history) 381 | 382 | self.output_text.moveCursor(QTextCursor.MoveOperation.End) 383 | self.output_text.insertPlainText(command) # Add the command to the current line 384 | self.output_text.append("") # Move to the next line 385 | 386 | if command.startswith("cd "): 387 | path = command[3:].strip() 388 | self.change_directory(path) 389 | else: 390 | self.process.start("cmd.exe", ["/c", command]) 391 | 392 | self.input_line.clear() 393 | 394 | def change_directory(self, path): 395 | try: 396 | if os.path.isdir(path): 397 | os.chdir(path) 398 | self.current_path = os.getcwd() 399 | self.update_file_list() 400 | else: 401 | self.output_text.append(f"'{path}' is not a valid directory.") 402 | except Exception as e: 403 | self.output_text.append(f"Error changing directory: {str(e)}") 404 | self.show_prompt() 405 | 406 | def update_file_list(self): 407 | try: 408 | files = os.listdir(self.current_path) 409 | model = QStringListModel(files) 410 | self.file_view.setModel(model) 411 | except Exception as e: 412 | self.output_text.append(f"Error listing files: {str(e)}") 413 | 414 | def handle_stdout(self): 415 | data = self.process.readAllStandardOutput().data().decode() 416 | self.output_text.insertPlainText(data) 417 | self.command_executed.emit(data, "") 418 | 419 | def handle_stderr(self): 420 | data = self.process.readAllStandardError().data().decode() 421 | self.output_text.insertPlainText(data) 422 | self.command_executed.emit("", data) 423 | 424 | def process_finished(self, exit_code, exit_status): 425 | self.show_prompt() 426 | 427 | def get_current_path(self): 428 | return self.current_path 429 | 430 | def keyPressEvent(self, event): 431 | if event.key() == Qt.Key.Key_Up: 432 | if self.history_index > 0: 433 | self.history_index -= 1 434 | self.input_line.setText(self.command_history[self.history_index]) 435 | elif event.key() == Qt.Key.Key_Down: 436 | if self.history_index < len(self.command_history) - 1: 437 | self.history_index += 1 438 | self.input_line.setText(self.command_history[self.history_index]) 439 | else: 440 | self.history_index = len(self.command_history) 441 | self.input_line.clear() 442 | 443 | class CombinedApp(QMainWindow): 444 | def __init__(self): 445 | super().__init__() 446 | self.setWindowTitle("PC Monitor & Terminal") 447 | self.setGeometry(100, 100, 900, 600) # Increased window size 448 | self.initUI() 449 | 450 | def initUI(self): 451 | central_widget = QWidget() 452 | self.setCentralWidget(central_widget) 453 | layout = QVBoxLayout(central_widget) 454 | 455 | # Create tab widget 456 | self.tab_widget = QTabWidget() 457 | layout.addWidget(self.tab_widget) 458 | 459 | # Create and add PC Monitor tab 460 | self.pc_monitor = PCMonitorWidget() 461 | self.tab_widget.addTab(self.pc_monitor, "PC Monitor") 462 | 463 | # Create and add Terminal tab 464 | self.terminal = TerminalWidget() 465 | self.tab_widget.addTab(self.terminal, "Terminal") 466 | 467 | self.terminal = NetworkWidget() 468 | self.tab_widget.addTab(self.terminal, "Network") 469 | 470 | # Set up timer for updating PC Monitor stats 471 | self.timer = QTimer(self) 472 | self.timer.timeout.connect(self.pc_monitor.update_stats) 473 | self.timer.start(1000) # Update every 1 second 474 | 475 | # Set dark theme colors 476 | self.set_dark_theme() 477 | 478 | def set_dark_theme(self): 479 | palette = self.palette() 480 | palette.setColor(QPalette.ColorRole.Window, QColor(30, 30, 30)) 481 | palette.setColor(QPalette.ColorRole.WindowText, QColor(220, 220, 220)) 482 | palette.setColor(QPalette.ColorRole.Base, QColor(15, 15, 15)) 483 | palette.setColor(QPalette.ColorRole.AlternateBase, QColor(30, 30, 30)) 484 | palette.setColor(QPalette.ColorRole.Text, QColor(220, 220, 220)) 485 | palette.setColor(QPalette.ColorRole.Button, QColor(60, 60, 60)) 486 | palette.setColor(QPalette.ColorRole.ButtonText, QColor(220, 220, 220)) 487 | self.setPalette(palette) 488 | 489 | def closeEvent(self, event): 490 | self.terminal.process.kill() 491 | event.accept() --------------------------------------------------------------------------------