├── .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 | 
4 | 
5 | 
6 | 
7 | 
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()
--------------------------------------------------------------------------------