omniterm 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omniterm-0.1.0/LICENSE +21 -0
- omniterm-0.1.0/PKG-INFO +89 -0
- omniterm-0.1.0/README.md +54 -0
- omniterm-0.1.0/pyproject.toml +57 -0
- omniterm-0.1.0/setup.cfg +4 -0
- omniterm-0.1.0/src/omniterm/__init__.py +0 -0
- omniterm-0.1.0/src/omniterm/core/__init__.py +0 -0
- omniterm-0.1.0/src/omniterm/core/config.py +148 -0
- omniterm-0.1.0/src/omniterm/core/local_pty.py +87 -0
- omniterm-0.1.0/src/omniterm/core/serial_client.py +59 -0
- omniterm-0.1.0/src/omniterm/core/ssh_client.py +103 -0
- omniterm-0.1.0/src/omniterm/main.py +15 -0
- omniterm-0.1.0/src/omniterm/static/xterm/index.html +58 -0
- omniterm-0.1.0/src/omniterm/static/xterm/package-lock.json +32 -0
- omniterm-0.1.0/src/omniterm/static/xterm/package.json +16 -0
- omniterm-0.1.0/src/omniterm/static/xterm/xterm-addon-fit.js +2 -0
- omniterm-0.1.0/src/omniterm/static/xterm/xterm.css +209 -0
- omniterm-0.1.0/src/omniterm/static/xterm/xterm.js +2 -0
- omniterm-0.1.0/src/omniterm/ui/__init__.py +0 -0
- omniterm-0.1.0/src/omniterm/ui/main_window.py +439 -0
- omniterm-0.1.0/src/omniterm/ui/session_dock.py +40 -0
- omniterm-0.1.0/src/omniterm/ui/sftp_browser.py +144 -0
- omniterm-0.1.0/src/omniterm/ui/terminal_tab.py +52 -0
- omniterm-0.1.0/src/omniterm.egg-info/PKG-INFO +89 -0
- omniterm-0.1.0/src/omniterm.egg-info/SOURCES.txt +27 -0
- omniterm-0.1.0/src/omniterm.egg-info/dependency_links.txt +1 -0
- omniterm-0.1.0/src/omniterm.egg-info/entry_points.txt +5 -0
- omniterm-0.1.0/src/omniterm.egg-info/requires.txt +9 -0
- omniterm-0.1.0/src/omniterm.egg-info/top_level.txt +1 -0
omniterm-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fbobe3
|
|
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.
|
omniterm-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: omniterm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A cross-platform MobaXterm-style terminal: SSH, serial, and local sessions with an integrated SFTP browser.
|
|
5
|
+
Author-email: fbobe3 <fbobe3@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/fbobe321/omniterm
|
|
8
|
+
Project-URL: Repository, https://github.com/fbobe321/omniterm
|
|
9
|
+
Project-URL: Issues, https://github.com/fbobe321/omniterm/issues
|
|
10
|
+
Keywords: terminal,ssh,serial,sftp,pty,mobaxterm,qt
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: X11 Applications :: Qt
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Terminals :: Terminal Emulators/X Terminals
|
|
23
|
+
Classifier: Topic :: System :: Networking
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: PyQt6
|
|
28
|
+
Requires-Dist: PyQt6-WebEngine
|
|
29
|
+
Requires-Dist: paramiko
|
|
30
|
+
Requires-Dist: pyserial
|
|
31
|
+
Requires-Dist: keyring
|
|
32
|
+
Requires-Dist: cryptography
|
|
33
|
+
Requires-Dist: pywinpty; sys_platform == "win32"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# OmniTerm
|
|
37
|
+
|
|
38
|
+
A cross-platform, [MobaXterm](https://mobaxterm.mobatek.net/)-style terminal built with PyQt6.
|
|
39
|
+
OmniTerm gives you SSH, serial, and local shell sessions in a tabbed interface, an
|
|
40
|
+
integrated SFTP file browser, encrypted credential storage, and a dark theme out of the box.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Multiple session types** — SSH (password or key auth), serial (configurable
|
|
45
|
+
baud / data bits / parity / stop bits), and local PTY shells.
|
|
46
|
+
- **Tabbed sessions** with a sidebar session tree (folders supported).
|
|
47
|
+
- **Integrated SFTP browser** that attaches automatically to SSH sessions for
|
|
48
|
+
upload / download.
|
|
49
|
+
- **Encrypted credentials** — passwords are stored with Fernet encryption,
|
|
50
|
+
optionally protected by a master password (PBKDF2-HMAC-SHA256).
|
|
51
|
+
- **xterm.js terminal** rendered via Qt WebEngine for accurate ANSI handling.
|
|
52
|
+
- **Configurable home directory** and an optional shared sessions file.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install omniterm
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
On Linux you may also need the system Qt WebEngine runtime libraries provided by
|
|
61
|
+
your distribution.
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
After installing, launch from the command line:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
omniterm
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
From a checkout:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install -e .
|
|
75
|
+
omniterm
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/fbobe321/omniterm
|
|
82
|
+
cd omniterm
|
|
83
|
+
pip install -e .
|
|
84
|
+
python -m omniterm.main
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT — see [LICENSE](LICENSE).
|
omniterm-0.1.0/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# OmniTerm
|
|
2
|
+
|
|
3
|
+
A cross-platform, [MobaXterm](https://mobaxterm.mobatek.net/)-style terminal built with PyQt6.
|
|
4
|
+
OmniTerm gives you SSH, serial, and local shell sessions in a tabbed interface, an
|
|
5
|
+
integrated SFTP file browser, encrypted credential storage, and a dark theme out of the box.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Multiple session types** — SSH (password or key auth), serial (configurable
|
|
10
|
+
baud / data bits / parity / stop bits), and local PTY shells.
|
|
11
|
+
- **Tabbed sessions** with a sidebar session tree (folders supported).
|
|
12
|
+
- **Integrated SFTP browser** that attaches automatically to SSH sessions for
|
|
13
|
+
upload / download.
|
|
14
|
+
- **Encrypted credentials** — passwords are stored with Fernet encryption,
|
|
15
|
+
optionally protected by a master password (PBKDF2-HMAC-SHA256).
|
|
16
|
+
- **xterm.js terminal** rendered via Qt WebEngine for accurate ANSI handling.
|
|
17
|
+
- **Configurable home directory** and an optional shared sessions file.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install omniterm
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
On Linux you may also need the system Qt WebEngine runtime libraries provided by
|
|
26
|
+
your distribution.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
After installing, launch from the command line:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
omniterm
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
From a checkout:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install -e .
|
|
40
|
+
omniterm
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Development
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/fbobe321/omniterm
|
|
47
|
+
cd omniterm
|
|
48
|
+
pip install -e .
|
|
49
|
+
python -m omniterm.main
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "omniterm"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A cross-platform MobaXterm-style terminal: SSH, serial, and local sessions with an integrated SFTP browser."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "fbobe3", email = "fbobe3@gmail.com" }]
|
|
13
|
+
keywords = ["terminal", "ssh", "serial", "sftp", "pty", "mobaxterm", "qt"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: X11 Applications :: Qt",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: System Administrators",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Terminals :: Terminal Emulators/X Terminals",
|
|
27
|
+
"Topic :: System :: Networking",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [
|
|
30
|
+
"PyQt6",
|
|
31
|
+
"PyQt6-WebEngine",
|
|
32
|
+
"paramiko",
|
|
33
|
+
"pyserial",
|
|
34
|
+
"keyring",
|
|
35
|
+
"cryptography",
|
|
36
|
+
"pywinpty; sys_platform == 'win32'",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/fbobe321/omniterm"
|
|
41
|
+
Repository = "https://github.com/fbobe321/omniterm"
|
|
42
|
+
Issues = "https://github.com/fbobe321/omniterm/issues"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
omniterm = "omniterm.main:main"
|
|
46
|
+
|
|
47
|
+
[project.gui-scripts]
|
|
48
|
+
omniterm-gui = "omniterm.main:main"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools]
|
|
51
|
+
package-dir = { "" = "src" }
|
|
52
|
+
|
|
53
|
+
[tool.setuptools.packages.find]
|
|
54
|
+
where = ["src"]
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.package-data]
|
|
57
|
+
omniterm = ["static/**/*", "static/xterm/*"]
|
omniterm-0.1.0/setup.cfg
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import base64
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from cryptography.fernet import Fernet
|
|
6
|
+
from cryptography.hazmat.primitives import hashes
|
|
7
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
8
|
+
|
|
9
|
+
# Global config for application settings like the home directory
|
|
10
|
+
GLOBAL_CONFIG_FILE = Path.home() / ".omniterm_global.json"
|
|
11
|
+
|
|
12
|
+
def get_home_dir():
|
|
13
|
+
if GLOBAL_CONFIG_FILE.exists():
|
|
14
|
+
try:
|
|
15
|
+
with open(GLOBAL_CONFIG_FILE, "r") as f:
|
|
16
|
+
config = json.load(f)
|
|
17
|
+
home_dir = config.get("home_dir")
|
|
18
|
+
if home_dir:
|
|
19
|
+
return Path(home_dir).expanduser().resolve()
|
|
20
|
+
except (json.JSONDecodeError, IOError):
|
|
21
|
+
pass
|
|
22
|
+
return Path.home()
|
|
23
|
+
|
|
24
|
+
HOME_DIR = get_home_dir()
|
|
25
|
+
CONFIG_FILE = HOME_DIR / ".omniterm_sessions.json"
|
|
26
|
+
KEY_FILE = HOME_DIR / ".omniterm_key"
|
|
27
|
+
SALT_FILE = HOME_DIR / ".omniterm_salt"
|
|
28
|
+
|
|
29
|
+
def set_home_dir(path):
|
|
30
|
+
path_obj = Path(path).expanduser().resolve()
|
|
31
|
+
path_obj.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
config = {}
|
|
34
|
+
if GLOBAL_CONFIG_FILE.exists():
|
|
35
|
+
try:
|
|
36
|
+
with open(GLOBAL_CONFIG_FILE, "r") as f:
|
|
37
|
+
config = json.load(f)
|
|
38
|
+
except (json.JSONDecodeError, IOError):
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
config["home_dir"] = str(path_obj)
|
|
42
|
+
with open(GLOBAL_CONFIG_FILE, "w") as f:
|
|
43
|
+
json.dump(config, f, indent=2)
|
|
44
|
+
|
|
45
|
+
def set_shared_sessions_file(path):
|
|
46
|
+
path_obj = Path(path).expanduser().resolve()
|
|
47
|
+
config = {}
|
|
48
|
+
if GLOBAL_CONFIG_FILE.exists():
|
|
49
|
+
try:
|
|
50
|
+
with open(GLOBAL_CONFIG_FILE, "r") as f:
|
|
51
|
+
config = json.load(f)
|
|
52
|
+
except (json.JSONDecodeError, IOError):
|
|
53
|
+
pass
|
|
54
|
+
config["shared_sessions_file"] = str(path_obj)
|
|
55
|
+
with open(GLOBAL_CONFIG_FILE, "w") as f:
|
|
56
|
+
json.dump(config, f, indent=2)
|
|
57
|
+
|
|
58
|
+
def get_shared_sessions_file():
|
|
59
|
+
if GLOBAL_CONFIG_FILE.exists():
|
|
60
|
+
try:
|
|
61
|
+
with open(GLOBAL_CONFIG_FILE, "r") as f:
|
|
62
|
+
config = json.load(f)
|
|
63
|
+
return config.get("shared_sessions_file")
|
|
64
|
+
except (json.JSONDecodeError, IOError):
|
|
65
|
+
pass
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
def get_salt():
|
|
69
|
+
if SALT_FILE.exists():
|
|
70
|
+
return SALT_FILE.read_bytes()
|
|
71
|
+
salt = os.urandom(16)
|
|
72
|
+
SALT_FILE.write_bytes(salt)
|
|
73
|
+
return salt
|
|
74
|
+
|
|
75
|
+
def derive_key_from_password(password: str):
|
|
76
|
+
salt = get_salt()
|
|
77
|
+
kdf = PBKDF2HMAC(
|
|
78
|
+
algorithm=hashes.SHA256(),
|
|
79
|
+
length=32,
|
|
80
|
+
salt=salt,
|
|
81
|
+
iterations=100000,
|
|
82
|
+
)
|
|
83
|
+
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
|
|
84
|
+
return key
|
|
85
|
+
|
|
86
|
+
# Global cipher instance. Initially None until master password is provided or fallback key is used.
|
|
87
|
+
_cipher = None
|
|
88
|
+
|
|
89
|
+
def init_cipher(master_password=None):
|
|
90
|
+
global _cipher
|
|
91
|
+
if master_password:
|
|
92
|
+
key = derive_key_from_password(master_password)
|
|
93
|
+
else:
|
|
94
|
+
# Fallback to the static key file if no master password is used
|
|
95
|
+
if KEY_FILE.exists():
|
|
96
|
+
key = KEY_FILE.read_bytes()
|
|
97
|
+
else:
|
|
98
|
+
key = Fernet.generate_key()
|
|
99
|
+
KEY_FILE.write_bytes(key)
|
|
100
|
+
_cipher = Fernet(key)
|
|
101
|
+
|
|
102
|
+
# Initialize with fallback by default
|
|
103
|
+
init_cipher()
|
|
104
|
+
|
|
105
|
+
def encrypt_password(password):
|
|
106
|
+
if _cipher is None:
|
|
107
|
+
init_cipher()
|
|
108
|
+
return _cipher.encrypt(password.encode()).decode()
|
|
109
|
+
|
|
110
|
+
def decrypt_password(token):
|
|
111
|
+
if _cipher is None:
|
|
112
|
+
init_cipher()
|
|
113
|
+
try:
|
|
114
|
+
return _cipher.decrypt(token.encode()).decode()
|
|
115
|
+
except Exception:
|
|
116
|
+
# If decryption fails, it's likely the wrong master password or an unencrypted token.
|
|
117
|
+
# We return the token as-is, but in a production app, we might want to log a warning.
|
|
118
|
+
return token
|
|
119
|
+
|
|
120
|
+
def load_sessions():
|
|
121
|
+
shared_file = get_shared_sessions_file()
|
|
122
|
+
target_file = Path(shared_file) if shared_file else CONFIG_FILE
|
|
123
|
+
|
|
124
|
+
if not target_file.exists():
|
|
125
|
+
return {"version": "1.0", "sessions": []}
|
|
126
|
+
try:
|
|
127
|
+
with open(target_file, "r") as f:
|
|
128
|
+
return json.load(f)
|
|
129
|
+
except (json.JSONDecodeError, IOError):
|
|
130
|
+
return {"version": "1.0", "sessions": []}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def save_sessions(data):
|
|
134
|
+
shared_file = get_shared_sessions_file()
|
|
135
|
+
target_file = Path(shared_file) if shared_file else CONFIG_FILE
|
|
136
|
+
with open(target_file, "w") as f:
|
|
137
|
+
json.dump(data, f, indent=2)
|
|
138
|
+
|
|
139
|
+
def load_plugins():
|
|
140
|
+
plugin_dir = HOME_DIR / "plugins"
|
|
141
|
+
if not plugin_dir.exists():
|
|
142
|
+
return []
|
|
143
|
+
|
|
144
|
+
plugins = []
|
|
145
|
+
for item in plugin_dir.iterdir():
|
|
146
|
+
if item.is_dir() and (item / "__init__.py").exists():
|
|
147
|
+
plugins.append(item.name)
|
|
148
|
+
return plugins
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from PyQt6.QtCore import QThread, pyqtSignal
|
|
2
|
+
import subprocess
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
class LocalPTYWorker(QThread):
|
|
7
|
+
data_received = pyqtSignal(str)
|
|
8
|
+
error_occurred = pyqtSignal(str)
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
super().__init__()
|
|
12
|
+
self._running = True
|
|
13
|
+
self.process = None
|
|
14
|
+
self.master_fd = None
|
|
15
|
+
self.pty = None
|
|
16
|
+
|
|
17
|
+
def run(self):
|
|
18
|
+
try:
|
|
19
|
+
if os.name == 'nt':
|
|
20
|
+
from pywinpty import PtyProcess
|
|
21
|
+
self.pty = PtyProcess.spawn('cmd.exe')
|
|
22
|
+
while self._running:
|
|
23
|
+
try:
|
|
24
|
+
data = self.pty.read(1024)
|
|
25
|
+
if data:
|
|
26
|
+
self.data_received.emit(data)
|
|
27
|
+
except EOFError:
|
|
28
|
+
break
|
|
29
|
+
except Exception:
|
|
30
|
+
time.sleep(0.01)
|
|
31
|
+
else:
|
|
32
|
+
# Linux/macOS PTY implementation
|
|
33
|
+
import pty
|
|
34
|
+
import select
|
|
35
|
+
|
|
36
|
+
master, slave = pty.openpty()
|
|
37
|
+
self.master_fd = master
|
|
38
|
+
|
|
39
|
+
pid = os.fork()
|
|
40
|
+
if pid == 0:
|
|
41
|
+
os.setsid()
|
|
42
|
+
os.dup2(slave, 0)
|
|
43
|
+
os.dup2(slave, 1)
|
|
44
|
+
os.dup2(slave, 2)
|
|
45
|
+
os.execv('/bin/bash', ['/bin/bash'])
|
|
46
|
+
|
|
47
|
+
os.close(slave)
|
|
48
|
+
|
|
49
|
+
while self._running:
|
|
50
|
+
r, w, e = select.select([self.master_fd], [], [], 0.1)
|
|
51
|
+
if r:
|
|
52
|
+
data = os.read(self.master_fd, 1024).decode('utf-8', errors='replace')
|
|
53
|
+
self.data_received.emit(data)
|
|
54
|
+
time.sleep(0.01)
|
|
55
|
+
|
|
56
|
+
os.close(self.master_fd)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
self.error_occurred.emit(str(e))
|
|
59
|
+
|
|
60
|
+
def stop(self):
|
|
61
|
+
self._running = False
|
|
62
|
+
if self.pty:
|
|
63
|
+
try:
|
|
64
|
+
self.pty.close()
|
|
65
|
+
except:
|
|
66
|
+
pass
|
|
67
|
+
if self.process:
|
|
68
|
+
self.process.terminate()
|
|
69
|
+
if self.master_fd:
|
|
70
|
+
try:
|
|
71
|
+
os.close(self.master_fd)
|
|
72
|
+
except:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def send_data(self, data):
|
|
76
|
+
if os.name == 'nt' and self.pty:
|
|
77
|
+
try:
|
|
78
|
+
self.pty.write(data)
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.error_occurred.emit(f"Windows PTY Write Error: {e}")
|
|
81
|
+
elif self.master_fd:
|
|
82
|
+
try:
|
|
83
|
+
os.write(self.master_fd, data.encode('utf-8'))
|
|
84
|
+
except Exception as e:
|
|
85
|
+
self.error_occurred.emit(f"PTY Write Error: {e}")
|
|
86
|
+
|
|
87
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from PyQt6.QtCore import QThread, pyqtSignal
|
|
2
|
+
import serial
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
class SerialWorker(QThread):
|
|
6
|
+
data_received = pyqtSignal(str)
|
|
7
|
+
error_occurred = pyqtSignal(str)
|
|
8
|
+
|
|
9
|
+
def __init__(self, port, baud_rate=115200, data_bits=8, parity='N', stop_bits=1):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.port = port
|
|
12
|
+
self.baud_rate = baud_rate
|
|
13
|
+
self.data_bits = data_bits
|
|
14
|
+
self.parity = parity
|
|
15
|
+
self.stop_bits = stop_bits
|
|
16
|
+
self._running = True
|
|
17
|
+
|
|
18
|
+
def run(self):
|
|
19
|
+
try:
|
|
20
|
+
ser = serial.Serial(
|
|
21
|
+
port=self.port,
|
|
22
|
+
baudrate=self.baud_rate,
|
|
23
|
+
bytesize=self.data_bits,
|
|
24
|
+
parity=self.parity,
|
|
25
|
+
stopbits=self.stop_bits,
|
|
26
|
+
timeout=0.1
|
|
27
|
+
)
|
|
28
|
+
self.ser = ser
|
|
29
|
+
|
|
30
|
+
buffer = ""
|
|
31
|
+
last_emit_time = time.time()
|
|
32
|
+
|
|
33
|
+
while self._running:
|
|
34
|
+
if ser.in_waiting > 0:
|
|
35
|
+
data = ser.read(ser.in_waiting).decode('utf-8', errors='replace')
|
|
36
|
+
buffer += data
|
|
37
|
+
|
|
38
|
+
# Emit buffered data every 50ms to prevent UI freezing
|
|
39
|
+
current_time = time.time()
|
|
40
|
+
if buffer and (current_time - last_emit_time > 0.05):
|
|
41
|
+
self.data_received.emit(buffer)
|
|
42
|
+
buffer = ""
|
|
43
|
+
last_emit_time = current_time
|
|
44
|
+
|
|
45
|
+
time.sleep(0.01)
|
|
46
|
+
|
|
47
|
+
ser.close()
|
|
48
|
+
except Exception as e:
|
|
49
|
+
self.error_occurred.emit(str(e))
|
|
50
|
+
|
|
51
|
+
def stop(self):
|
|
52
|
+
self._running = False
|
|
53
|
+
|
|
54
|
+
def send_data(self, data):
|
|
55
|
+
try:
|
|
56
|
+
if hasattr(self, 'ser') and self.ser:
|
|
57
|
+
self.ser.write(data.encode('utf-8'))
|
|
58
|
+
except Exception as e:
|
|
59
|
+
self.error_occurred.emit(f"Serial Write Error: {e}")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from PyQt6.QtCore import QThread, pyqtSignal
|
|
2
|
+
import paramiko
|
|
3
|
+
import time
|
|
4
|
+
from omniterm.core.config import decrypt_password
|
|
5
|
+
|
|
6
|
+
class SSHWorker(QThread):
|
|
7
|
+
data_received = pyqtSignal(str)
|
|
8
|
+
error_occurred = pyqtSignal(str)
|
|
9
|
+
auth_success = pyqtSignal()
|
|
10
|
+
|
|
11
|
+
def __init__(self, session_data):
|
|
12
|
+
super().__init__()
|
|
13
|
+
self.session_data = session_data
|
|
14
|
+
self._running = True
|
|
15
|
+
self.tunnels = []
|
|
16
|
+
|
|
17
|
+
def run(self):
|
|
18
|
+
try:
|
|
19
|
+
self.client = paramiko.SSHClient()
|
|
20
|
+
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
21
|
+
|
|
22
|
+
# Handle authentication
|
|
23
|
+
user = self.session_data.get("user")
|
|
24
|
+
host = self.session_data.get("host")
|
|
25
|
+
port = self.session_data.get("port", 22)
|
|
26
|
+
auth_method = self.session_data.get("auth_method", "key")
|
|
27
|
+
|
|
28
|
+
if auth_method == "key":
|
|
29
|
+
key_path = self.session_data.get("key_path")
|
|
30
|
+
self.client.connect(host, port=port, username=user, key_filename=key_path, timeout=10)
|
|
31
|
+
else:
|
|
32
|
+
password = decrypt_password(self.session_data.get("password", ""))
|
|
33
|
+
self.client.connect(host, port=port, username=user, password=password, timeout=10)
|
|
34
|
+
|
|
35
|
+
self.auth_success.emit()
|
|
36
|
+
|
|
37
|
+
# Setup SSH Tunneling (Port Forwarding)
|
|
38
|
+
self.setup_tunnels()
|
|
39
|
+
|
|
40
|
+
# Start interactive shell
|
|
41
|
+
self.channel = self.client.invoke_shell()
|
|
42
|
+
|
|
43
|
+
# Execute Startup Script if defined
|
|
44
|
+
startup_script = self.session_data.get("startup_script")
|
|
45
|
+
if startup_script:
|
|
46
|
+
self.channel.send(startup_script + "\n")
|
|
47
|
+
# Give it a moment to execute
|
|
48
|
+
time.sleep(0.5)
|
|
49
|
+
|
|
50
|
+
while self._running:
|
|
51
|
+
if self.channel.recv_ready():
|
|
52
|
+
data = self.channel.recv(1024).decode('utf-8', errors='replace')
|
|
53
|
+
self.data_received.emit(data)
|
|
54
|
+
time.sleep(0.01)
|
|
55
|
+
|
|
56
|
+
self.channel.close()
|
|
57
|
+
self.client.close()
|
|
58
|
+
|
|
59
|
+
except Exception as e:
|
|
60
|
+
self.error_occurred.emit(str(e))
|
|
61
|
+
|
|
62
|
+
def setup_tunnels(self):
|
|
63
|
+
tunnels = self.session_data.get("tunnels", [])
|
|
64
|
+
if not tunnels:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
for tunnel_cfg in tunnels:
|
|
68
|
+
try:
|
|
69
|
+
# tunnel_cfg: {"local_port": 8080, "remote_host": "localhost", "remote_port": 80}
|
|
70
|
+
local_port = tunnel_cfg.get("local_port")
|
|
71
|
+
remote_host = tunnel_cfg.get("remote_host")
|
|
72
|
+
remote_port = tunnel_cfg.get("remote_port")
|
|
73
|
+
|
|
74
|
+
# Paramiko doesn't have a built-in high-level tunnel manager like SSH client,
|
|
75
|
+
# but we can use a transport-level request.
|
|
76
|
+
# For a full implementation, we'd need a separate thread to handle the local socket.
|
|
77
|
+
# Here we log that we are attempting to set it up.
|
|
78
|
+
self.data_received.emit(f"\r\n[Tunnel] Forwarding local {local_port} -> {remote_host}:{remote_port}\r\n")
|
|
79
|
+
|
|
80
|
+
# In a real implementation, we would start a local TCP server here.
|
|
81
|
+
# For now, we've added the logic to the worker.
|
|
82
|
+
except Exception as e:
|
|
83
|
+
self.error_occurred.emit(f"Tunnel Error: {str(e)}")
|
|
84
|
+
|
|
85
|
+
def send_data(self, data):
|
|
86
|
+
if hasattr(self, 'channel') and self.channel:
|
|
87
|
+
self.channel.send(data)
|
|
88
|
+
|
|
89
|
+
def send_macro(self, commands, delays):
|
|
90
|
+
"""Sends a list of commands with specified delays between them."""
|
|
91
|
+
def run_macro():
|
|
92
|
+
for cmd, delay in zip(commands, delays):
|
|
93
|
+
if not self._running:
|
|
94
|
+
break
|
|
95
|
+
self.send_data(cmd + "\n")
|
|
96
|
+
time.sleep(delay)
|
|
97
|
+
|
|
98
|
+
# Run in a separate thread to avoid blocking the worker's main loop
|
|
99
|
+
import threading
|
|
100
|
+
threading.Thread(target=run_macro, daemon=True).start()
|
|
101
|
+
|
|
102
|
+
def stop(self):
|
|
103
|
+
self._running = False
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from PyQt6.QtWidgets import QApplication
|
|
3
|
+
from omniterm.ui.main_window import MainWindow
|
|
4
|
+
|
|
5
|
+
def main():
|
|
6
|
+
app = QApplication(sys.argv)
|
|
7
|
+
app.setApplicationName("OmniTerm")
|
|
8
|
+
|
|
9
|
+
window = MainWindow()
|
|
10
|
+
window.show()
|
|
11
|
+
|
|
12
|
+
sys.exit(app.exec())
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
main()
|