# PyQt6 SSH Terminal Widget: UglierPTY
## Overview
UglierPTY is a proof-of-concept (POC) application created to explore the possibility of constructing a fully functional SSH Terminal Widget using PyQt6. This POC is inspired by its sister project, UglyPTY, a full-featured SSH client that utilizes xterm.js. The aim is to offer a similar, if not superior, feature set without relying on browser technologies or JavaScript, resulting in a more streamlined and resource-efficient solution.
## Features
- Fully functional SSH terminal built with PyQt6
- Communication class to manage SSH communication in real-time
- Resizable terminal window
- Terminal history and screen ratio control via pyte
- Modular design for easy embedding into other PyQt6 projects
- Free of Web technology (no xterm.js)
## Prerequisites
- Python 3.x
- PyQt6
- Paramiko
- pyte
## Installation
1. Clone the repository:
```bash
git clone https://github.com/scottpeterman/UglierPTY
```
2. Navigate into the project directory:
```bash
cd UglierPTY
```
3. Install the required Python packages:
```bash
pip install -r requirements.txt
```
## Usage
The primary entry point for this POC is the `uglierpty.py` script. To launch the application:
```bash
python uglierpty.py
```
Upon execution, you'll be presented with a dialog box asking for SSH credentials (host, username, and password). After successful authentication, the SSH Terminal interface will appear.
## Packet Flow
1. **Keyboard Input**: The user interacts with the PyQt6 terminal widget by providing keyboard input.
2. **Terminal Frontend**: The input is captured by the PyQt6 terminal frontend and passed to the Listener object.
3. **SSHLib and Communication**: The `SSHLib` class serves as the backend for SSH communication, managed by a Communication object running on a separate thread. The Communication class uses the Paramiko library to handle SSH communication with the host.
4. **Host Interaction**: Communication class sends the user input to the remote SSH host.
5. **Data Retrieval**: Communication class listens for incoming data from the remote host using `select.select()` and updates the terminal screen with the received data.
6. **Display**: The PyQt6 terminal widget displays the data, effectively showing the SSH session output.
Absolutely. Let's dive deeper into the mechanics of screen rendering and cursor handling, focusing on the calculations and data structures involved.
---
### Screen Management - Detailed Explanation
#### Screen Rendering
##### Data Structure: The Buffer Grid
- A 2D array of cells, where each cell contains:
- `char`: The character to display.
- `fg_color`: Foreground color.
- `bg_color`: Background color.
- `attributes`: Additional attributes like bold, underline, etc.
```python
buffer_grid = [
[{"char": "H", "fg_color": "#FFFFFF", "bg_color": "#000000", "attributes": None},
{"char": "e", "fg_color": "#FFFFFF", "bg_color": "#000000", "attributes": None},
...],
[...],
...
]
```
##### PaintEvent: The Rendering Engine
- Qt's `paintEvent(QPaintEvent *event)` is overridden.
- A `QPainter` object is used for all graphical rendering.
```python
def paintEvent(self, event):
painter = QPainter(self)
# Logic to paint each cell of buffer_grid
```
##### Calculating Cell Position
- Cell width and height are calculated based on widget dimensions and grid size.
```python
cell_width = self.width() // num_columns
cell_height = self.height() // num_rows
```
- To draw a cell at `(row, col)`:
```python
x_position = col * cell_width
y_position = row * cell_height
```
##### Text and Attributes
- To draw text:
```python
painter.setPen(QColor(fg_color))
painter.drawText(x_position, y_position, char)
```
- To apply attributes like bold or underline:
```python
font = painter.font()
font.setBold(True if "bold" in attributes else False)
painter.setFont(font)
```
#### Cursor Handling
##### Cursor Position
- Stored as `(cursor_x, cursor_y)` in the 2D buffer_grid.
##### Rendering the Cursor
- Change the background color of the cell `(cursor_x, cursor_y)` during `paintEvent`.
```python
if (row, col) == (cursor_x, cursor_y):
painter.fillRect(x_position, y_position, cell_width, cell_height, QColor(cursor_bg_color))
```
##### Cursor Movement Calculations
- Moving the cursor involves changing `(cursor_x, cursor_y)` and constraining it within the grid dimensions.
```python
# Move cursor down
cursor_x = min(cursor_x + 1, num_rows - 1)
# Move cursor up
cursor_x = max(cursor_x - 1, 0)
```
##### Cursor Blinking
- A `QTimer` toggles a boolean flag, which is then checked during each `paintEvent`.
```python
self.cursor_visible = not self.cursor_visible
self.update() # Trigger a repaint
```
---
This should offer a detailed explanation of how screen rendering and cursor handling work in the SSHTerminal. Feel free to add code snippets or expand on these points to suit your implementation.
Raw data
{
"_id": null,
"home_page": "https://github.com/scottpeterman/UglierPTY",
"name": "uglierpty",
"maintainer": "",
"docs_url": null,
"requires_python": "",
"maintainer_email": "",
"keywords": "",
"author": "Scott Peterman",
"author_email": "scottpeterman@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/61/a7/a485198d70e55c14eaa24bd5ec5e2a031a972b80c5597349317ca3af68ec/uglierpty-0.2.tar.gz",
"platform": null,
"description": "# PyQt6 SSH Terminal Widget: UglierPTY\r\n\r\n## Overview\r\n\r\nUglierPTY is a proof-of-concept (POC) application created to explore the possibility of constructing a fully functional SSH Terminal Widget using PyQt6. This POC is inspired by its sister project, UglyPTY, a full-featured SSH client that utilizes xterm.js. The aim is to offer a similar, if not superior, feature set without relying on browser technologies or JavaScript, resulting in a more streamlined and resource-efficient solution.\r\n\r\n## Features\r\n\r\n- Fully functional SSH terminal built with PyQt6\r\n- Communication class to manage SSH communication in real-time\r\n- Resizable terminal window\r\n- Terminal history and screen ratio control via pyte\r\n- Modular design for easy embedding into other PyQt6 projects\r\n- Free of Web technology (no xterm.js)\r\n\r\n## Prerequisites\r\n\r\n- Python 3.x\r\n- PyQt6\r\n- Paramiko\r\n- pyte\r\n\r\n## Installation\r\n\r\n1. Clone the repository:\r\n\r\n ```bash\r\n git clone https://github.com/scottpeterman/UglierPTY\r\n ```\r\n\r\n2. Navigate into the project directory:\r\n\r\n ```bash\r\n cd UglierPTY\r\n ```\r\n\r\n3. Install the required Python packages:\r\n\r\n ```bash\r\n pip install -r requirements.txt\r\n ```\r\n\r\n## Usage\r\n\r\nThe primary entry point for this POC is the `uglierpty.py` script. To launch the application:\r\n\r\n```bash\r\npython uglierpty.py\r\n```\r\n\r\nUpon execution, you'll be presented with a dialog box asking for SSH credentials (host, username, and password). After successful authentication, the SSH Terminal interface will appear.\r\n\r\n## Packet Flow\r\n\r\n1. **Keyboard Input**: The user interacts with the PyQt6 terminal widget by providing keyboard input.\r\n \r\n2. **Terminal Frontend**: The input is captured by the PyQt6 terminal frontend and passed to the Listener object.\r\n\r\n3. **SSHLib and Communication**: The `SSHLib` class serves as the backend for SSH communication, managed by a Communication object running on a separate thread. The Communication class uses the Paramiko library to handle SSH communication with the host.\r\n\r\n4. **Host Interaction**: Communication class sends the user input to the remote SSH host.\r\n\r\n5. **Data Retrieval**: Communication class listens for incoming data from the remote host using `select.select()` and updates the terminal screen with the received data.\r\n\r\n6. **Display**: The PyQt6 terminal widget displays the data, effectively showing the SSH session output.\r\n\r\nAbsolutely. Let's dive deeper into the mechanics of screen rendering and cursor handling, focusing on the calculations and data structures involved.\r\n\r\n---\r\n\r\n### Screen Management - Detailed Explanation\r\n\r\n#### Screen Rendering\r\n\r\n##### Data Structure: The Buffer Grid\r\n- A 2D array of cells, where each cell contains:\r\n - `char`: The character to display.\r\n - `fg_color`: Foreground color.\r\n - `bg_color`: Background color.\r\n - `attributes`: Additional attributes like bold, underline, etc.\r\n\r\n```python\r\nbuffer_grid = [\r\n [{\"char\": \"H\", \"fg_color\": \"#FFFFFF\", \"bg_color\": \"#000000\", \"attributes\": None},\r\n {\"char\": \"e\", \"fg_color\": \"#FFFFFF\", \"bg_color\": \"#000000\", \"attributes\": None},\r\n ...],\r\n [...],\r\n ...\r\n]\r\n```\r\n\r\n##### PaintEvent: The Rendering Engine\r\n- Qt's `paintEvent(QPaintEvent *event)` is overridden.\r\n- A `QPainter` object is used for all graphical rendering.\r\n\r\n```python\r\ndef paintEvent(self, event):\r\n painter = QPainter(self)\r\n # Logic to paint each cell of buffer_grid\r\n```\r\n\r\n##### Calculating Cell Position\r\n- Cell width and height are calculated based on widget dimensions and grid size.\r\n \r\n```python\r\ncell_width = self.width() // num_columns\r\ncell_height = self.height() // num_rows\r\n```\r\n\r\n- To draw a cell at `(row, col)`:\r\n\r\n```python\r\nx_position = col * cell_width\r\ny_position = row * cell_height\r\n```\r\n\r\n##### Text and Attributes\r\n- To draw text:\r\n\r\n```python\r\npainter.setPen(QColor(fg_color))\r\npainter.drawText(x_position, y_position, char)\r\n```\r\n\r\n- To apply attributes like bold or underline:\r\n\r\n```python\r\nfont = painter.font()\r\nfont.setBold(True if \"bold\" in attributes else False)\r\npainter.setFont(font)\r\n```\r\n\r\n#### Cursor Handling\r\n\r\n##### Cursor Position\r\n- Stored as `(cursor_x, cursor_y)` in the 2D buffer_grid.\r\n\r\n##### Rendering the Cursor\r\n- Change the background color of the cell `(cursor_x, cursor_y)` during `paintEvent`.\r\n\r\n```python\r\nif (row, col) == (cursor_x, cursor_y):\r\n painter.fillRect(x_position, y_position, cell_width, cell_height, QColor(cursor_bg_color))\r\n```\r\n\r\n##### Cursor Movement Calculations\r\n- Moving the cursor involves changing `(cursor_x, cursor_y)` and constraining it within the grid dimensions.\r\n\r\n```python\r\n# Move cursor down\r\ncursor_x = min(cursor_x + 1, num_rows - 1)\r\n\r\n# Move cursor up\r\ncursor_x = max(cursor_x - 1, 0)\r\n```\r\n\r\n##### Cursor Blinking\r\n- A `QTimer` toggles a boolean flag, which is then checked during each `paintEvent`.\r\n\r\n```python\r\nself.cursor_visible = not self.cursor_visible\r\nself.update() # Trigger a repaint\r\n```\r\n\r\n---\r\n\r\nThis should offer a detailed explanation of how screen rendering and cursor handling work in the SSHTerminal. Feel free to add code snippets or expand on these points to suit your implementation.\r\n",
"bugtrack_url": null,
"license": "GPLv3",
"summary": "UglierPTY a POC for SSH UI in PyQt6",
"version": "0.2",
"project_urls": {
"Homepage": "https://github.com/scottpeterman/UglierPTY"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "1e2e64feac82a6e630e4587cb33466614cd5497cd2a7eaec7b6fa2a42a909e61",
"md5": "ece837954d9014e60eed4f78c9f33a95",
"sha256": "0c484cc8bbbfe5aeee00c7f74edb3fc2dc663451df435180ca314a0e80d6aefd"
},
"downloads": -1,
"filename": "uglierpty-0.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "ece837954d9014e60eed4f78c9f33a95",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 11230,
"upload_time": "2023-09-19T22:57:51",
"upload_time_iso_8601": "2023-09-19T22:57:51.942851Z",
"url": "https://files.pythonhosted.org/packages/1e/2e/64feac82a6e630e4587cb33466614cd5497cd2a7eaec7b6fa2a42a909e61/uglierpty-0.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "61a7a485198d70e55c14eaa24bd5ec5e2a031a972b80c5597349317ca3af68ec",
"md5": "6f57e81fecd3d56c0897ca1a6c12a032",
"sha256": "2b75705b9b29da5f0836744756e86b2ca7591bf708278368682ae9de7b1468bf"
},
"downloads": -1,
"filename": "uglierpty-0.2.tar.gz",
"has_sig": false,
"md5_digest": "6f57e81fecd3d56c0897ca1a6c12a032",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 12457,
"upload_time": "2023-09-19T22:57:53",
"upload_time_iso_8601": "2023-09-19T22:57:53.617010Z",
"url": "https://files.pythonhosted.org/packages/61/a7/a485198d70e55c14eaa24bd5ec5e2a031a972b80c5597349317ca3af68ec/uglierpty-0.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-09-19 22:57:53",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "scottpeterman",
"github_project": "UglierPTY",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"requirements": [],
"lcname": "uglierpty"
}