19 Commits

Author SHA1 Message Date
Keith Smith
2b25f805cd Complete Milestone 4 interactive SSH session UX 2026-03-01 11:00:31 -07:00
Keith Smith
776ddc1a53 Integrate KodoTerm for SSH terminal sessions 2026-03-01 10:36:06 -07:00
Keith Smith
c3369b8e48 Improve terminal theming, cursor UX, and size negotiation 2026-03-01 10:14:43 -07:00
Keith Smith
20ee48db32 Add ANSI color rendering and terminal themes 2026-03-01 10:08:04 -07:00
Keith Smith
2b4f498259 Allow direct terminal typing and collapsible session panels 2026-03-01 09:58:21 -07:00
Keith Smith
614d31fa71 Restore built-in askpass helper for SSH password auth 2026-03-01 09:53:17 -07:00
Keith Smith
ceed19d517 Draft Milestone 4 scope and mark progress in docs 2026-03-01 09:50:08 -07:00
Keith Smith
2ea712db36 Start Milestone 4 interactive SSH terminal and host-key flow 2026-03-01 09:50:03 -07:00
Keith Smith
3c158269bf Fix SSH askpass helper text-file-busy race 2026-03-01 09:42:32 -07:00
Keith Smith
e2a8b874d7 Document Milestone 3 deliverables and SSH prerequisites 2026-03-01 09:37:43 -07:00
Keith Smith
71a2f2e868 Expand profile editor for SSH auth and host policy fields 2026-03-01 09:37:37 -07:00
Keith Smith
6a4bcb75eb Add threaded session backend architecture with real SSH backend 2026-03-01 09:37:34 -07:00
Keith Smith
582c57bc5f Auto-fill standard port when protocol changes 2026-03-01 09:24:31 -07:00
Keith Smith
01762422e9 Document Milestone 2 scope and completion 2026-03-01 09:21:57 -07:00
Keith Smith
f8a81ebe36 Implement Milestone 2 profile schema, dialog, and connect lifecycle 2026-03-01 09:21:53 -07:00
Keith Smith
87b0f60569 Update progress for Milestone 1 completion 2026-03-01 09:06:36 -07:00
Keith Smith
09c54313af Implement SQLite-backed profile CRUD in Profiles window 2026-03-01 09:06:34 -07:00
Keith Smith
fba7336f69 Add SQLite profile repository and Qt SQL dependency 2026-03-01 09:06:31 -07:00
Keith Smith
6d147894c5 Add cross-platform development environment setup guide 2026-03-01 09:02:53 -07:00
523 changed files with 25750 additions and 79 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build/

View File

@@ -10,18 +10,36 @@ set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets) find_package(Qt6 6.2 REQUIRED COMPONENTS Widgets Sql)
qt_standard_project_setup() qt_standard_project_setup()
add_subdirectory(third_party/KodoTerm)
add_executable(orbithub add_executable(orbithub
src/main.cpp src/main.cpp
src/profile_dialog.cpp
src/profile_dialog.h
src/profile_repository.cpp
src/profile_repository.h
src/profiles_window.cpp src/profiles_window.cpp
src/profiles_window.h src/profiles_window.h
src/session_backend.h
src/session_backend_factory.cpp
src/session_backend_factory.h
src/session_tab.cpp
src/session_tab.h
src/terminal_view.cpp
src/terminal_view.h
src/session_window.cpp src/session_window.cpp
src/session_window.h src/session_window.h
src/ssh_session_backend.cpp
src/ssh_session_backend.h
src/unsupported_session_backend.cpp
src/unsupported_session_backend.h
) )
target_link_libraries(orbithub PRIVATE Qt6::Widgets) target_link_libraries(orbithub PRIVATE Qt6::Widgets Qt6::Sql)
target_link_libraries(orbithub PRIVATE KodoTerm::KodoTerm)
install(TARGETS orbithub RUNTIME DESTINATION bin) install(TARGETS orbithub RUNTIME DESTINATION bin)

59
docs/BUILDING.md Normal file
View File

@@ -0,0 +1,59 @@
# Building OrbitHub (C++ / Qt6 Widgets)
Run all commands from the repository root unless noted.
## Linux (Ubuntu / Mint)
```bash
sudo apt update
sudo apt install -y \
build-essential cmake ninja-build git pkg-config \
qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools \
openssh-client
cmake -S . -B build -G Ninja
cmake --build build
./build/orbithub
```
## macOS (Homebrew)
```bash
xcode-select --install
brew update
brew install cmake ninja pkg-config qt@6 openssh
cmake -S . -B build -G Ninja -DCMAKE_PREFIX_PATH="$(brew --prefix qt@6)"
cmake --build build
./build/orbithub
```
## Windows 11 (PowerShell + MSVC + vcpkg)
```powershell
winget install -e --id Git.Git
winget install -e --id Kitware.CMake
winget install -e --id Ninja-build.Ninja
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
winget install -e --id Microsoft.VisualStudio.2022.BuildTools `
--override "--quiet --wait --norestart --add Microsoft.VisualStudio.Workload.VCTools"
```
Open a new terminal after installs, then:
```powershell
git clone https://github.com/microsoft/vcpkg C:\dev\vcpkg
C:\dev\vcpkg\bootstrap-vcpkg.bat
C:\dev\vcpkg\vcpkg.exe install qtbase:x64-windows
cmake -S . -B build -G Ninja `
-DCMAKE_TOOLCHAIN_FILE=C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build build
.\build\orbithub.exe
```
## Notes
- OrbitHub currently requires Qt6 Widgets and CMake 3.21+.
- Milestone 3 SSH sessions require an `ssh` client available on `PATH`.
- If Qt is installed in a custom location, pass `-DCMAKE_PREFIX_PATH=/path/to/Qt/6.x.x/<toolchain>` to CMake.

View File

@@ -54,3 +54,40 @@ OrbitHub uses a two-window model:
- Profiles CRUD - Profiles CRUD
- Connect loads profile name into tab - Connect loads profile name into tab
- Tag: v0-m1-done - Tag: v0-m1-done
---
## Milestone 2
- Extend profiles schema (`host`, `port`, `username`, `protocol`, `auth_mode`)
- Replace prompt-based create/edit with a structured profile dialog form
- Connect loads full profile details into session tab
- Session lifecycle states in UI (`Connecting`, `Connected`, `Failed`) with non-blocking updates
- Tag: v0-m2-done
---
## Milestone 3
- Real SSH backend using native `ssh` process (connect, disconnect, reconnect)
- Protocol backend abstraction with worker-thread execution
- RDP/VNC explicitly marked as not implemented in session UX
- Connect-time credential prompts (password/private key path) with no secret storage in DB
- Session tab controls: `Connect`, `Disconnect`, `Reconnect`, `Copy Error`
- Per-session timestamped event log and user-friendly error mapping
- Profile schema extended with `private_key_path` and `known_hosts_policy`
- Tag: v0-m3-done
---
## Milestone 4 (Draft)
- Make SSH sessions actually usable as interactive sessions inside OrbitHub
- Replace placeholder surface with interactive SSH terminal panel (stream output + send input)
- Add SSH host-key trust prompt flow for `Ask` policy
- Improve auth flow UX for password / private key at connect time
- Preserve responsive UI under active session output
- Add terminal/session utilities (`Clear Terminal`, improved output visibility)
- Keep session controls stable (`Connect`, `Disconnect`, `Reconnect`, `Copy Error`)
- Add validation and diagnostics coverage for host-key, auth, and reconnect behavior
- Planned Tag: v0-m4-done (after completion approval)

View File

@@ -11,7 +11,70 @@ Delivered:
- `SessionWindow` (`QMainWindow`) with `QTabWidget` - `SessionWindow` (`QMainWindow`) with `QTabWidget`
- Placeholder tab content showing `OrbitHub Native Surface` - Placeholder tab content showing `OrbitHub Native Surface`
- `main.cpp` wiring for application startup - `main.cpp` wiring for application startup
- Cross-platform build command guide in `docs/BUILDING.md`
Git: Git:
- Branch: `milestone-0-restart-cpp`
- Tag: `v0-m0-done` - Tag: `v0-m0-done`
## Milestone 1 - Storage and CRUD
Status: Completed
Delivered:
- SQLite integration via Qt SQL (`QSQLITE`)
- Persistent profile database bootstrap (`profiles` table)
- Profiles CRUD (New / Edit / Delete) in `ProfilesWindow`
- Search-backed profile listing from storage
- Double-click connect opens `SessionWindow` tab with selected profile name
Git:
- Tag: `v0-m1-done`
## Milestone 2 - Profile Details and Connect Lifecycle
Status: Completed
Delivered:
- SQLite schema migration for profile details (`host`, `port`, `username`, `protocol`, `auth_mode`)
- New `ProfileDialog` form for New/Edit profile workflows
- Profiles list now shows endpoint metadata and supports search by name or host
- Connect now loads complete profile details into `SessionWindow`
- Session tab lifecycle status updates (`Connecting`, `Connected`, `Failed`) via non-blocking timer flow
Git:
- Tag: `v0-m2-done`
## Milestone 3 - Real SSH Backend and Session Controls
Status: Completed
Delivered:
- Backend architecture introduced (`SessionBackend` + protocol-specific implementations)
- Worker-thread backend execution for connection lifecycle operations
- Real SSH process backend (`ssh`) with connect/disconnect/reconnect
- Unsupported protocol backend with explicit not-implemented messaging (RDP/VNC)
- Session tab controls: `Connect`, `Disconnect`, `Reconnect`, `Copy Error`
- Connect-time credential flow (password prompt / private-key path selection)
- Session event log pane with timestamps and user-friendly error mapping
- SQLite profile schema migration for `private_key_path` and `known_hosts_policy`
Git:
- Tag: `v0-m3-done`
## Milestone 4 - Interactive SSH Session UX
Status: Completed
Delivered:
- Embedded interactive SSH terminal using `KodoTerm` + vendored `libvterm`
- Native in-terminal typing for SSH sessions (no separate input box)
- ANSI/color rendering with selectable terminal themes (`Dark`, `Light`, `Solarized Dark`)
- Cross-platform SSH auth path improvements (`ssh-askpass` handling and host-key policy wiring)
- Session UX simplification: auto-connect on tab open, disconnect on tab close
- Tab-state indicators via tab color and state suffix (`Connecting`, `Connected`, `Disconnected`, `Failed`)
- Right-click tab menu for `Disconnect`, `Reconnect`, `Theme`, and `Clear`
- Collapsible events panel retained as primary diagnostics surface; inline detail/status banners removed
- Terminal behavior polish: better fixed-width font selection, cursor visibility, backspace handling, and terminal-size negotiation stability
Git:
- Tag: `v0-m4-done` (pending push)

View File

@@ -4,6 +4,8 @@
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
Q_INIT_RESOURCE(KodoTermThemes);
QApplication app(argc, argv); QApplication app(argc, argv);
ProfilesWindow window; ProfilesWindow window;

196
src/profile_dialog.cpp Normal file
View File

@@ -0,0 +1,196 @@
#include "profile_dialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFileDialog>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QSignalBlocker>
#include <QSpinBox>
#include <QVBoxLayout>
namespace {
int standardPortForProtocol(const QString& protocol)
{
if (protocol == QStringLiteral("RDP")) {
return 3389;
}
if (protocol == QStringLiteral("VNC")) {
return 5900;
}
return 22; // SSH default
}
}
ProfileDialog::ProfileDialog(QWidget* parent)
: QDialog(parent),
m_nameInput(new QLineEdit(this)),
m_hostInput(new QLineEdit(this)),
m_portInput(new QSpinBox(this)),
m_usernameInput(new QLineEdit(this)),
m_protocolInput(new QComboBox(this)),
m_authModeInput(new QComboBox(this)),
m_privateKeyPathInput(new QLineEdit(this)),
m_browsePrivateKeyButton(new QPushButton(QStringLiteral("Browse"), this)),
m_knownHostsPolicyInput(new QComboBox(this))
{
resize(520, 340);
auto* layout = new QVBoxLayout(this);
auto* form = new QFormLayout();
m_nameInput->setPlaceholderText(QStringLiteral("Production Bastion"));
m_hostInput->setPlaceholderText(QStringLiteral("example.internal"));
m_portInput->setRange(1, 65535);
m_portInput->setValue(22);
m_usernameInput->setPlaceholderText(QStringLiteral("deploy"));
m_protocolInput->addItems({QStringLiteral("SSH"), QStringLiteral("RDP"), QStringLiteral("VNC")});
m_authModeInput->addItems({QStringLiteral("Password"), QStringLiteral("Private Key")});
m_knownHostsPolicyInput->addItems(
{QStringLiteral("Ask"), QStringLiteral("Strict"), QStringLiteral("Accept New"), QStringLiteral("Ignore")});
m_privateKeyPathInput->setPlaceholderText(QStringLiteral("/home/user/.ssh/id_ed25519"));
auto* privateKeyRow = new QWidget(this);
auto* privateKeyLayout = new QHBoxLayout(privateKeyRow);
privateKeyLayout->setContentsMargins(0, 0, 0, 0);
privateKeyLayout->addWidget(m_privateKeyPathInput, 1);
privateKeyLayout->addWidget(m_browsePrivateKeyButton);
connect(m_browsePrivateKeyButton,
&QPushButton::clicked,
this,
[this]() {
const QString selected = QFileDialog::getOpenFileName(this,
QStringLiteral("Select Private Key"),
QString(),
QStringLiteral("All Files (*)"));
if (!selected.isEmpty()) {
m_privateKeyPathInput->setText(selected);
}
});
connect(m_protocolInput,
&QComboBox::currentTextChanged,
this,
[this](const QString& protocol) {
m_portInput->setValue(standardPortForProtocol(protocol));
refreshAuthFields();
});
connect(m_authModeInput,
&QComboBox::currentTextChanged,
this,
[this](const QString&) { refreshAuthFields(); });
form->addRow(QStringLiteral("Name"), m_nameInput);
form->addRow(QStringLiteral("Host"), m_hostInput);
form->addRow(QStringLiteral("Port"), m_portInput);
form->addRow(QStringLiteral("Username"), m_usernameInput);
form->addRow(QStringLiteral("Protocol"), m_protocolInput);
form->addRow(QStringLiteral("Auth Mode"), m_authModeInput);
form->addRow(QStringLiteral("Private Key"), privateKeyRow);
form->addRow(QStringLiteral("Known Hosts"), m_knownHostsPolicyInput);
auto* note = new QLabel(
QStringLiteral("Passwords are requested at connect time and are not stored."),
this);
note->setWordWrap(true);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
layout->addLayout(form);
layout->addWidget(note);
layout->addWidget(buttons);
refreshAuthFields();
}
void ProfileDialog::setDialogTitle(const QString& title)
{
setWindowTitle(title);
}
void ProfileDialog::setProfile(const Profile& profile)
{
m_nameInput->setText(profile.name);
m_hostInput->setText(profile.host);
m_portInput->setValue(profile.port > 0 ? profile.port : 22);
m_usernameInput->setText(profile.username);
m_privateKeyPathInput->setText(profile.privateKeyPath);
const int protocolIndex = m_protocolInput->findText(profile.protocol);
{
// Keep stored custom port when loading an existing profile.
const QSignalBlocker blocker(m_protocolInput);
m_protocolInput->setCurrentIndex(protocolIndex >= 0 ? protocolIndex : 0);
}
const int authModeIndex = m_authModeInput->findText(profile.authMode);
m_authModeInput->setCurrentIndex(authModeIndex >= 0 ? authModeIndex : 0);
const int knownHostsIndex = m_knownHostsPolicyInput->findText(profile.knownHostsPolicy);
m_knownHostsPolicyInput->setCurrentIndex(knownHostsIndex >= 0 ? knownHostsIndex : 0);
refreshAuthFields();
}
Profile ProfileDialog::profile() const
{
Profile profile;
profile.id = -1;
profile.name = m_nameInput->text().trimmed();
profile.host = m_hostInput->text().trimmed();
profile.port = m_portInput->value();
profile.username = m_usernameInput->text().trimmed();
profile.protocol = m_protocolInput->currentText();
profile.authMode = m_authModeInput->currentText();
profile.privateKeyPath = m_privateKeyPathInput->text().trimmed();
profile.knownHostsPolicy = m_knownHostsPolicyInput->currentText();
return profile;
}
void ProfileDialog::accept()
{
if (m_nameInput->text().trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Validation Error"),
QStringLiteral("Profile name is required."));
return;
}
if (m_hostInput->text().trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Validation Error"),
QStringLiteral("Host is required."));
return;
}
if (m_protocolInput->currentText() == QStringLiteral("SSH")
&& m_usernameInput->text().trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Validation Error"),
QStringLiteral("Username is required for SSH profiles."));
return;
}
QDialog::accept();
}
void ProfileDialog::refreshAuthFields()
{
const bool isSsh = m_protocolInput->currentText() == QStringLiteral("SSH");
const bool isPrivateKey = m_authModeInput->currentText() == QStringLiteral("Private Key");
m_authModeInput->setEnabled(isSsh);
m_privateKeyPathInput->setEnabled(isSsh && isPrivateKey);
m_browsePrivateKeyButton->setEnabled(isSsh && isPrivateKey);
m_knownHostsPolicyInput->setEnabled(isSsh);
}

41
src/profile_dialog.h Normal file
View File

@@ -0,0 +1,41 @@
#ifndef ORBITHUB_PROFILE_DIALOG_H
#define ORBITHUB_PROFILE_DIALOG_H
#include "profile_repository.h"
#include <QDialog>
class QComboBox;
class QLineEdit;
class QPushButton;
class QSpinBox;
class ProfileDialog : public QDialog
{
Q_OBJECT
public:
explicit ProfileDialog(QWidget* parent = nullptr);
void setDialogTitle(const QString& title);
void setProfile(const Profile& profile);
Profile profile() const;
protected:
void accept() override;
private:
QLineEdit* m_nameInput;
QLineEdit* m_hostInput;
QSpinBox* m_portInput;
QLineEdit* m_usernameInput;
QComboBox* m_protocolInput;
QComboBox* m_authModeInput;
QLineEdit* m_privateKeyPathInput;
QPushButton* m_browsePrivateKeyButton;
QComboBox* m_knownHostsPolicyInput;
void refreshAuthFields();
};
#endif

323
src/profile_repository.cpp Normal file
View File

@@ -0,0 +1,323 @@
#include "profile_repository.h"
#include <QDir>
#include <QSet>
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QStandardPaths>
#include <QVariant>
namespace {
QString buildDatabasePath()
{
QString appDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
if (appDataPath.isEmpty()) {
appDataPath = QDir::currentPath();
}
QDir dataDir(appDataPath);
dataDir.mkpath(QStringLiteral("."));
return dataDir.filePath(QStringLiteral("orbithub_profiles.sqlite"));
}
void bindProfileFields(QSqlQuery& query, const Profile& profile)
{
query.addBindValue(profile.name.trimmed());
query.addBindValue(profile.host.trimmed());
query.addBindValue(profile.port);
query.addBindValue(profile.username.trimmed());
query.addBindValue(profile.protocol.trimmed());
query.addBindValue(profile.authMode.trimmed());
query.addBindValue(profile.privateKeyPath.trimmed());
query.addBindValue(profile.knownHostsPolicy.trimmed().isEmpty()
? QStringLiteral("Ask")
: profile.knownHostsPolicy.trimmed());
}
Profile profileFromQuery(const QSqlQuery& query)
{
Profile profile;
profile.id = query.value(0).toLongLong();
profile.name = query.value(1).toString();
profile.host = query.value(2).toString();
profile.port = query.value(3).toInt();
profile.username = query.value(4).toString();
profile.protocol = query.value(5).toString();
profile.authMode = query.value(6).toString();
profile.privateKeyPath = query.value(7).toString();
profile.knownHostsPolicy = query.value(8).toString();
if (profile.knownHostsPolicy.isEmpty()) {
profile.knownHostsPolicy = QStringLiteral("Ask");
}
return profile;
}
bool isProfileValid(const Profile& profile)
{
return !profile.name.trimmed().isEmpty() && !profile.host.trimmed().isEmpty()
&& profile.port >= 1 && profile.port <= 65535;
}
}
ProfileRepository::ProfileRepository() : m_connectionName(QStringLiteral("orbithub_main"))
{
if (!initializeDatabase()) {
QSqlDatabase::removeDatabase(m_connectionName);
}
}
ProfileRepository::~ProfileRepository()
{
if (QSqlDatabase::contains(m_connectionName)) {
QSqlDatabase db = QSqlDatabase::database(m_connectionName);
if (db.isOpen()) {
db.close();
}
}
QSqlDatabase::removeDatabase(m_connectionName);
}
QString ProfileRepository::initError() const
{
return m_initError;
}
QString ProfileRepository::lastError() const
{
return m_lastError;
}
std::vector<Profile> ProfileRepository::listProfiles(const QString& searchQuery) const
{
std::vector<Profile> result;
if (!QSqlDatabase::contains(m_connectionName)) {
return result;
}
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName));
if (searchQuery.trimmed().isEmpty()) {
query.prepare(QStringLiteral(
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
"FROM profiles "
"ORDER BY lower(name) ASC, id ASC"));
} else {
query.prepare(QStringLiteral(
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
"FROM profiles "
"WHERE lower(name) LIKE lower(?) OR lower(host) LIKE lower(?) "
"ORDER BY lower(name) ASC, id ASC"));
const QString search = QStringLiteral("%") + searchQuery.trimmed() + QStringLiteral("%");
query.addBindValue(search);
query.addBindValue(search);
}
if (!query.exec()) {
setLastError(query.lastError().text());
return result;
}
while (query.next()) {
result.push_back(profileFromQuery(query));
}
return result;
}
std::optional<Profile> ProfileRepository::getProfile(qint64 id) const
{
if (!QSqlDatabase::contains(m_connectionName)) {
return std::nullopt;
}
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"SELECT id, name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy "
"FROM profiles WHERE id = ?"));
query.addBindValue(id);
if (!query.exec()) {
setLastError(query.lastError().text());
return std::nullopt;
}
if (!query.next()) {
return std::nullopt;
}
return profileFromQuery(query);
}
std::optional<Profile> ProfileRepository::createProfile(const Profile& profile) const
{
if (!QSqlDatabase::contains(m_connectionName)) {
return std::nullopt;
}
setLastError(QString());
if (!isProfileValid(profile)) {
setLastError(QStringLiteral("Name, host, and a valid port are required."));
return std::nullopt;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"INSERT INTO profiles(name, host, port, username, protocol, auth_mode, private_key_path, known_hosts_policy) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)"));
bindProfileFields(query, profile);
if (!query.exec()) {
setLastError(query.lastError().text());
return std::nullopt;
}
Profile created = profile;
created.id = query.lastInsertId().toLongLong();
return created;
}
bool ProfileRepository::updateProfile(const Profile& profile) const
{
if (!QSqlDatabase::contains(m_connectionName)) {
return false;
}
setLastError(QString());
if (profile.id < 0 || !isProfileValid(profile)) {
setLastError(QStringLiteral("Invalid profile data."));
return false;
}
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral(
"UPDATE profiles "
"SET name = ?, host = ?, port = ?, username = ?, protocol = ?, auth_mode = ?, private_key_path = ?, known_hosts_policy = ? "
"WHERE id = ?"));
bindProfileFields(query, profile);
query.addBindValue(profile.id);
if (!query.exec()) {
setLastError(query.lastError().text());
return false;
}
return query.numRowsAffected() > 0;
}
bool ProfileRepository::deleteProfile(qint64 id) const
{
if (!QSqlDatabase::contains(m_connectionName)) {
return false;
}
setLastError(QString());
QSqlQuery query(QSqlDatabase::database(m_connectionName));
query.prepare(QStringLiteral("DELETE FROM profiles WHERE id = ?"));
query.addBindValue(id);
if (!query.exec()) {
setLastError(query.lastError().text());
return false;
}
return query.numRowsAffected() > 0;
}
bool ProfileRepository::initializeDatabase()
{
QSqlDatabase database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName);
database.setDatabaseName(buildDatabasePath());
if (!database.open()) {
m_initError = database.lastError().text();
return false;
}
QSqlQuery query(database);
const bool created = query.exec(QStringLiteral(
"CREATE TABLE IF NOT EXISTS profiles ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL UNIQUE,"
"host TEXT NOT NULL DEFAULT '',"
"port INTEGER NOT NULL DEFAULT 22,"
"username TEXT NOT NULL DEFAULT '',"
"protocol TEXT NOT NULL DEFAULT 'SSH',"
"auth_mode TEXT NOT NULL DEFAULT 'Password',"
"private_key_path TEXT NOT NULL DEFAULT '',"
"known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'"
")"));
if (!created) {
m_initError = query.lastError().text();
return false;
}
if (!ensureProfileSchema()) {
m_initError = m_lastError;
return false;
}
return true;
}
bool ProfileRepository::ensureProfileSchema() const
{
if (!QSqlDatabase::contains(m_connectionName)) {
setLastError(QStringLiteral("Database connection missing."));
return false;
}
QSqlQuery tableInfo(QSqlDatabase::database(m_connectionName));
if (!tableInfo.exec(QStringLiteral("PRAGMA table_info(profiles)"))) {
setLastError(tableInfo.lastError().text());
return false;
}
QSet<QString> columns;
while (tableInfo.next()) {
columns.insert(tableInfo.value(1).toString());
}
struct ColumnDef {
QString name;
QString ddl;
};
const std::vector<ColumnDef> required = {
{QStringLiteral("host"), QStringLiteral("ALTER TABLE profiles ADD COLUMN host TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("port"), QStringLiteral("ALTER TABLE profiles ADD COLUMN port INTEGER NOT NULL DEFAULT 22")},
{QStringLiteral("username"), QStringLiteral("ALTER TABLE profiles ADD COLUMN username TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("protocol"), QStringLiteral("ALTER TABLE profiles ADD COLUMN protocol TEXT NOT NULL DEFAULT 'SSH'")},
{QStringLiteral("auth_mode"), QStringLiteral("ALTER TABLE profiles ADD COLUMN auth_mode TEXT NOT NULL DEFAULT 'Password'")},
{QStringLiteral("private_key_path"), QStringLiteral("ALTER TABLE profiles ADD COLUMN private_key_path TEXT NOT NULL DEFAULT ''")},
{QStringLiteral("known_hosts_policy"), QStringLiteral("ALTER TABLE profiles ADD COLUMN known_hosts_policy TEXT NOT NULL DEFAULT 'Ask'")}};
for (const ColumnDef& column : required) {
if (columns.contains(column.name)) {
continue;
}
QSqlQuery alter(QSqlDatabase::database(m_connectionName));
if (!alter.exec(column.ddl)) {
setLastError(alter.lastError().text());
return false;
}
}
setLastError(QString());
return true;
}
void ProfileRepository::setLastError(const QString& error) const
{
m_lastError = error;
}

48
src/profile_repository.h Normal file
View File

@@ -0,0 +1,48 @@
#ifndef ORBITHUB_PROFILE_REPOSITORY_H
#define ORBITHUB_PROFILE_REPOSITORY_H
#include <QString>
#include <QtGlobal>
#include <optional>
#include <vector>
struct Profile
{
qint64 id = -1;
QString name;
QString host;
int port = 22;
QString username;
QString protocol = QStringLiteral("SSH");
QString authMode = QStringLiteral("Password");
QString privateKeyPath;
QString knownHostsPolicy = QStringLiteral("Ask");
};
class ProfileRepository
{
public:
ProfileRepository();
~ProfileRepository();
QString initError() const;
QString lastError() const;
std::vector<Profile> listProfiles(const QString& searchQuery = QString()) const;
std::optional<Profile> getProfile(qint64 id) const;
std::optional<Profile> createProfile(const Profile& profile) const;
bool updateProfile(const Profile& profile) const;
bool deleteProfile(qint64 id) const;
private:
QString m_connectionName;
QString m_initError;
mutable QString m_lastError;
bool initializeDatabase();
bool ensureProfileSchema() const;
void setLastError(const QString& error) const;
};
#endif

View File

@@ -1,5 +1,7 @@
#include "profiles_window.h" #include "profiles_window.h"
#include "profile_dialog.h"
#include "profile_repository.h"
#include "session_window.h" #include "session_window.h"
#include <algorithm> #include <algorithm>
@@ -12,25 +14,50 @@
#include <QListWidgetItem> #include <QListWidgetItem>
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
#include <QStringList> #include <QVariant>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidget> #include <QWidget>
namespace {
QString formatProfileListItem(const Profile& profile)
{
return QStringLiteral("%1 [%2 %3:%4]")
.arg(profile.name, profile.protocol, profile.host, QString::number(profile.port));
}
}
ProfilesWindow::ProfilesWindow(QWidget* parent) ProfilesWindow::ProfilesWindow(QWidget* parent)
: QMainWindow(parent), : QMainWindow(parent),
m_searchBox(nullptr), m_searchBox(nullptr),
m_profilesList(nullptr), m_profilesList(nullptr),
m_newButton(nullptr), m_newButton(nullptr),
m_editButton(nullptr), m_editButton(nullptr),
m_deleteButton(nullptr) m_deleteButton(nullptr),
m_repository(std::make_unique<ProfileRepository>())
{ {
setWindowTitle(QStringLiteral("OrbitHub Profiles")); setWindowTitle(QStringLiteral("OrbitHub Profiles"));
resize(520, 620); resize(640, 620);
setupUi(); setupUi();
populateSampleProfiles();
if (!m_repository->initError().isEmpty()) {
QMessageBox::critical(this,
QStringLiteral("Database Error"),
QStringLiteral("Failed to initialize SQLite database: %1")
.arg(m_repository->initError()));
m_newButton->setEnabled(false);
m_editButton->setEnabled(false);
m_deleteButton->setEnabled(false);
m_searchBox->setEnabled(false);
m_profilesList->setEnabled(false);
return;
} }
loadProfiles();
}
ProfilesWindow::~ProfilesWindow() = default;
void ProfilesWindow::setupUi() void ProfilesWindow::setupUi()
{ {
auto* central = new QWidget(this); auto* central = new QWidget(this);
@@ -38,7 +65,7 @@ void ProfilesWindow::setupUi()
auto* searchLabel = new QLabel(QStringLiteral("Search"), central); auto* searchLabel = new QLabel(QStringLiteral("Search"), central);
m_searchBox = new QLineEdit(central); m_searchBox = new QLineEdit(central);
m_searchBox->setPlaceholderText(QStringLiteral("Filter profiles...")); m_searchBox->setPlaceholderText(QStringLiteral("Filter by name or host..."));
m_profilesList = new QListWidget(central); m_profilesList = new QListWidget(central);
m_profilesList->setSelectionMode(QAbstractItemView::SingleSelection); m_profilesList->setSelectionMode(QAbstractItemView::SingleSelection);
@@ -63,49 +90,155 @@ void ProfilesWindow::setupUi()
connect(m_searchBox, connect(m_searchBox,
&QLineEdit::textChanged, &QLineEdit::textChanged,
this, this,
[this](const QString& text) { filterProfiles(text); }); [this](const QString& text) { loadProfiles(text); });
connect(m_profilesList, connect(m_profilesList,
&QListWidget::itemDoubleClicked, &QListWidget::itemDoubleClicked,
this, this,
[this](QListWidgetItem* item) { openSessionForItem(item); }); [this](QListWidgetItem* item) { openSessionForItem(item); });
// Milestone 0 keeps profile management as placeholders. connect(m_newButton, &QPushButton::clicked, this, [this]() { createProfile(); });
auto showTodo = [this](const QString& action) { connect(m_editButton, &QPushButton::clicked, this, [this]() { editSelectedProfile(); });
connect(m_deleteButton, &QPushButton::clicked, this, [this]() { deleteSelectedProfile(); });
}
void ProfilesWindow::loadProfiles(const QString& query)
{
m_profilesList->clear();
m_profileCache.clear();
const std::vector<Profile> profiles = m_repository->listProfiles(query);
if (!m_repository->lastError().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Load Profiles"),
QStringLiteral("Failed to load profiles: %1")
.arg(m_repository->lastError()));
return;
}
for (const Profile& profile : profiles) {
auto* item = new QListWidgetItem(formatProfileListItem(profile), m_profilesList);
item->setData(Qt::UserRole, QVariant::fromValue(profile.id));
item->setToolTip(QStringLiteral("%1://%2@%3:%4\nAuth: %5\nKnown Hosts: %6")
.arg(profile.protocol,
profile.username.isEmpty() ? QStringLiteral("<none>") : profile.username,
profile.host,
QString::number(profile.port),
profile.authMode,
profile.knownHostsPolicy));
m_profileCache.insert_or_assign(profile.id, profile);
}
}
std::optional<Profile> ProfilesWindow::selectedProfile() const
{
QListWidgetItem* item = m_profilesList->currentItem();
if (item == nullptr) {
return std::nullopt;
}
const QVariant value = item->data(Qt::UserRole);
if (!value.isValid()) {
return std::nullopt;
}
const qint64 id = value.toLongLong();
const auto cacheIt = m_profileCache.find(id);
if (cacheIt != m_profileCache.end()) {
return cacheIt->second;
}
return m_repository->getProfile(id);
}
void ProfilesWindow::createProfile()
{
ProfileDialog dialog(this);
dialog.setDialogTitle(QStringLiteral("New Profile"));
if (dialog.exec() != QDialog::Accepted) {
return;
}
if (!m_repository->createProfile(dialog.profile()).has_value()) {
QMessageBox::warning(this,
QStringLiteral("Create Profile"),
QStringLiteral("Failed to create profile: %1")
.arg(m_repository->lastError().isEmpty()
? QStringLiteral("unknown error")
: m_repository->lastError()));
return;
}
loadProfiles(m_searchBox->text());
}
void ProfilesWindow::editSelectedProfile()
{
const std::optional<Profile> selected = selectedProfile();
if (!selected.has_value()) {
QMessageBox::information(this, QMessageBox::information(this,
QStringLiteral("Milestone 0"), QStringLiteral("Edit Profile"),
QStringLiteral("%1 is planned for Milestone 1.").arg(action)); QStringLiteral("Select a profile first."));
}; return;
}
connect(m_newButton, &QPushButton::clicked, this, [showTodo]() { showTodo(QStringLiteral("New Profile")); }); ProfileDialog dialog(this);
connect(m_editButton, &QPushButton::clicked, this, [showTodo]() { showTodo(QStringLiteral("Edit Profile")); }); dialog.setDialogTitle(QStringLiteral("Edit Profile"));
connect(m_deleteButton, dialog.setProfile(selected.value());
&QPushButton::clicked,
if (dialog.exec() != QDialog::Accepted) {
return;
}
Profile updated = dialog.profile();
updated.id = selected->id;
if (!m_repository->updateProfile(updated)) {
QMessageBox::warning(this,
QStringLiteral("Edit Profile"),
QStringLiteral("Failed to update profile: %1")
.arg(m_repository->lastError().isEmpty()
? QStringLiteral("unknown error")
: m_repository->lastError()));
return;
}
loadProfiles(m_searchBox->text());
}
void ProfilesWindow::deleteSelectedProfile()
{
const std::optional<Profile> selected = selectedProfile();
if (!selected.has_value()) {
QMessageBox::information(this,
QStringLiteral("Delete Profile"),
QStringLiteral("Select a profile first."));
return;
}
const QMessageBox::StandardButton confirm = QMessageBox::question(
this, this,
[showTodo]() { showTodo(QStringLiteral("Delete Profile")); }); QStringLiteral("Delete Profile"),
QStringLiteral("Delete profile '%1'?").arg(selected->name),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No);
if (confirm != QMessageBox::Yes) {
return;
} }
void ProfilesWindow::populateSampleProfiles() if (!m_repository->deleteProfile(selected->id)) {
{ QMessageBox::warning(this,
const QStringList sampleProfiles = { QStringLiteral("Delete Profile"),
QStringLiteral("Production Bastion"), QStringLiteral("Failed to delete profile: %1")
QStringLiteral("Staging API Host"), .arg(m_repository->lastError().isEmpty()
QStringLiteral("Local Lab VM"), ? QStringLiteral("unknown error")
QStringLiteral("CI Build Agent")}; : m_repository->lastError()));
return;
m_profilesList->addItems(sampleProfiles);
} }
void ProfilesWindow::filterProfiles(const QString& query) loadProfiles(m_searchBox->text());
{
const QString trimmed = query.trimmed();
for (int i = 0; i < m_profilesList->count(); ++i) {
QListWidgetItem* item = m_profilesList->item(i);
const bool matches = trimmed.isEmpty()
|| item->text().contains(trimmed, Qt::CaseInsensitive);
item->setHidden(!matches);
}
} }
void ProfilesWindow::openSessionForItem(QListWidgetItem* item) void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
@@ -114,7 +247,24 @@ void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
return; return;
} }
auto* session = new SessionWindow(item->text()); const QVariant value = item->data(Qt::UserRole);
if (!value.isValid()) {
return;
}
const qint64 id = value.toLongLong();
const std::optional<Profile> profile = m_repository->getProfile(id);
if (!profile.has_value()) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("Failed to load profile for session: %1")
.arg(m_repository->lastError().isEmpty()
? QStringLiteral("profile not found")
: m_repository->lastError()));
return;
}
auto* session = new SessionWindow(profile.value());
session->setAttribute(Qt::WA_DeleteOnClose); session->setAttribute(Qt::WA_DeleteOnClose);
m_sessionWindows.emplace_back(session); m_sessionWindows.emplace_back(session);

View File

@@ -1,10 +1,16 @@
#ifndef ORBITHUB_PROFILES_WINDOW_H #ifndef ORBITHUB_PROFILES_WINDOW_H
#define ORBITHUB_PROFILES_WINDOW_H #define ORBITHUB_PROFILES_WINDOW_H
#include "profile_repository.h"
#include <QMainWindow> #include <QMainWindow>
#include <QString> #include <QString>
#include <QtGlobal>
#include <memory>
#include <optional>
#include <QPointer> #include <QPointer>
#include <unordered_map>
#include <vector> #include <vector>
class QListWidget; class QListWidget;
@@ -19,6 +25,7 @@ class ProfilesWindow : public QMainWindow
public: public:
explicit ProfilesWindow(QWidget* parent = nullptr); explicit ProfilesWindow(QWidget* parent = nullptr);
~ProfilesWindow() override;
private: private:
QLineEdit* m_searchBox; QLineEdit* m_searchBox;
@@ -27,10 +34,15 @@ private:
QPushButton* m_editButton; QPushButton* m_editButton;
QPushButton* m_deleteButton; QPushButton* m_deleteButton;
std::vector<QPointer<SessionWindow>> m_sessionWindows; std::vector<QPointer<SessionWindow>> m_sessionWindows;
std::unique_ptr<ProfileRepository> m_repository;
std::unordered_map<qint64, Profile> m_profileCache;
void setupUi(); void setupUi();
void populateSampleProfiles(); void loadProfiles(const QString& query = QString());
void filterProfiles(const QString& query); std::optional<Profile> selectedProfile() const;
void createProfile();
void editSelectedProfile();
void deleteSelectedProfile();
void openSessionForItem(QListWidgetItem* item); void openSessionForItem(QListWidgetItem* item);
}; };

62
src/session_backend.h Normal file
View File

@@ -0,0 +1,62 @@
#ifndef ORBITHUB_SESSION_BACKEND_H
#define ORBITHUB_SESSION_BACKEND_H
#include "profile_repository.h"
#include <QObject>
#include <QString>
class SessionConnectOptions
{
public:
QString password;
QString privateKeyPath;
QString knownHostsPolicy;
};
enum class SessionState {
Disconnected,
Connecting,
Connected,
Failed,
};
class SessionBackend : public QObject
{
Q_OBJECT
public:
explicit SessionBackend(const Profile& profile, QObject* parent = nullptr)
: QObject(parent), m_profile(profile)
{
}
~SessionBackend() override = default;
const Profile& profile() const
{
return m_profile;
}
public slots:
virtual void connectSession(const SessionConnectOptions& options) = 0;
virtual void disconnectSession() = 0;
virtual void reconnectSession(const SessionConnectOptions& options) = 0;
virtual void sendInput(const QString& input) = 0;
virtual void confirmHostKey(bool trustHost) = 0;
virtual void updateTerminalSize(int columns, int rows) = 0;
signals:
void stateChanged(SessionState state, const QString& message);
void eventLogged(const QString& message);
void connectionError(const QString& displayMessage, const QString& rawMessage);
void outputReceived(const QString& text);
void hostKeyConfirmationRequested(const QString& prompt);
private:
Profile m_profile;
};
Q_DECLARE_METATYPE(SessionConnectOptions)
Q_DECLARE_METATYPE(SessionState)
#endif

View File

@@ -0,0 +1,14 @@
#include "session_backend_factory.h"
#include "session_backend.h"
#include "ssh_session_backend.h"
#include "unsupported_session_backend.h"
std::unique_ptr<SessionBackend> createSessionBackend(const Profile& profile)
{
if (profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0) {
return std::make_unique<SshSessionBackend>(profile);
}
return std::make_unique<UnsupportedSessionBackend>(profile);
}

View File

@@ -0,0 +1,12 @@
#ifndef ORBITHUB_SESSION_BACKEND_FACTORY_H
#define ORBITHUB_SESSION_BACKEND_FACTORY_H
#include "profile_repository.h"
#include <memory>
class SessionBackend;
std::unique_ptr<SessionBackend> createSessionBackend(const Profile& profile);
#endif

670
src/session_tab.cpp Normal file
View File

@@ -0,0 +1,670 @@
#include "session_tab.h"
#include "session_backend_factory.h"
#include "terminal_view.h"
#include <KodoTerm/KodoTerm.hpp>
#include <QDateTime>
#include <QFileDialog>
#include <QFileInfo>
#include <QFont>
#include <QFontDatabase>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QPlainTextEdit>
#include <QProcessEnvironment>
#include <QThread>
#include <QTimer>
#include <QToolButton>
#include <QVBoxLayout>
#include <memory>
namespace {
QFont defaultTerminalFont()
{
QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
font.setStyleHint(QFont::Monospace);
font.setFixedPitch(true);
font.setKerning(false);
font.setLetterSpacing(QFont::AbsoluteSpacing, 0.0);
return font;
}
TerminalTheme themeForName(const QString& themeName)
{
if (themeName.compare(QStringLiteral("Light"), Qt::CaseInsensitive) == 0) {
return TerminalTheme::loadKonsoleTheme(
QStringLiteral(":/KodoTermThemes/konsole/BlackOnWhite.colorscheme"));
}
if (themeName.compare(QStringLiteral("Solarized Dark"), Qt::CaseInsensitive) == 0) {
return TerminalTheme::loadKonsoleTheme(
QStringLiteral(":/KodoTermThemes/konsole/Solarized.colorscheme"));
}
return TerminalTheme::loadKonsoleTheme(
QStringLiteral(":/KodoTermThemes/konsole/Breeze.colorscheme"));
}
}
SessionTab::SessionTab(const Profile& profile, QWidget* parent)
: QWidget(parent),
m_profile(profile),
m_backendThread(nullptr),
m_backend(nullptr),
m_useKodoTermForSsh(profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive)
== 0),
m_state(SessionState::Disconnected),
m_terminalThemeName(QStringLiteral("Dark")),
m_sshTerminal(nullptr),
m_terminalOutput(nullptr),
m_eventLog(nullptr),
m_toggleEventsButton(nullptr),
m_eventsPanel(nullptr)
{
qRegisterMetaType<SessionConnectOptions>("SessionConnectOptions");
qRegisterMetaType<SessionState>("SessionState");
setupUi();
if (m_useKodoTermForSsh) {
connect(m_sshTerminal,
&KodoTerm::finished,
this,
[this](int exitCode, int) {
if (m_state == SessionState::Disconnected) {
return;
}
if (m_state == SessionState::Connected) {
if (exitCode != 0) {
appendEvent(QStringLiteral("SSH session closed with exit code %1.")
.arg(exitCode));
}
setState(SessionState::Disconnected,
QStringLiteral("SSH session closed."));
return;
}
if (exitCode == 0) {
setState(SessionState::Disconnected,
QStringLiteral("SSH session ended."));
return;
}
m_lastError = QStringLiteral("ssh exited with code %1").arg(exitCode);
appendEvent(QStringLiteral("Error: %1").arg(m_lastError));
setState(SessionState::Failed,
QStringLiteral("SSH session exited unexpectedly."));
});
connect(m_sshTerminal,
&KodoTerm::cwdChanged,
this,
[this](const QString& cwd) {
if (!cwd.trimmed().isEmpty()) {
appendEvent(QStringLiteral("Remote cwd: %1").arg(cwd));
}
});
} else {
m_backendThread = new QThread(this);
std::unique_ptr<SessionBackend> backend = createSessionBackend(m_profile);
m_backend = backend.release();
m_backend->moveToThread(m_backendThread);
connect(m_backendThread, &QThread::finished, m_backend, &QObject::deleteLater);
connect(this,
&SessionTab::requestConnect,
m_backend,
&SessionBackend::connectSession,
Qt::QueuedConnection);
connect(this,
&SessionTab::requestDisconnect,
m_backend,
&SessionBackend::disconnectSession,
Qt::QueuedConnection);
connect(this,
&SessionTab::requestReconnect,
m_backend,
&SessionBackend::reconnectSession,
Qt::QueuedConnection);
connect(this,
&SessionTab::requestInput,
m_backend,
&SessionBackend::sendInput,
Qt::QueuedConnection);
connect(this,
&SessionTab::requestHostKeyConfirmation,
m_backend,
&SessionBackend::confirmHostKey,
Qt::QueuedConnection);
connect(this,
&SessionTab::requestTerminalSize,
m_backend,
&SessionBackend::updateTerminalSize,
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::stateChanged,
this,
&SessionTab::onBackendStateChanged,
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::eventLogged,
this,
&SessionTab::onBackendEventLogged,
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::connectionError,
this,
&SessionTab::onBackendConnectionError,
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::outputReceived,
this,
&SessionTab::onBackendOutputReceived,
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::hostKeyConfirmationRequested,
this,
&SessionTab::onBackendHostKeyConfirmationRequested,
Qt::QueuedConnection);
m_backendThread->start();
}
setState(SessionState::Disconnected, QStringLiteral("Ready to connect."));
QTimer::singleShot(0, this, &SessionTab::connectSession);
}
SessionTab::~SessionTab()
{
if (m_useKodoTermForSsh && m_sshTerminal != nullptr && m_state != SessionState::Disconnected) {
m_sshTerminal->kill();
}
if (m_backend != nullptr && m_backendThread != nullptr && m_backendThread->isRunning()) {
QMetaObject::invokeMethod(m_backend, "disconnectSession", Qt::BlockingQueuedConnection);
m_backendThread->quit();
m_backendThread->wait(2000);
}
}
QString SessionTab::tabTitle() const
{
return QStringLiteral("%1 (%2)").arg(m_profile.name, stateSuffix());
}
void SessionTab::connectSession()
{
if (m_state == SessionState::Connecting || m_state == SessionState::Connected) {
return;
}
if (!validateProfileForConnect()) {
return;
}
const std::optional<SessionConnectOptions> options = buildConnectOptions();
if (!options.has_value()) {
return;
}
m_lastConnectOptions = options.value();
if (m_useKodoTermForSsh) {
if (!startSshTerminal(options.value())) {
return;
}
return;
}
emit requestConnect(options.value());
}
void SessionTab::disconnectSession()
{
if (m_state == SessionState::Disconnected) {
return;
}
if (m_useKodoTermForSsh) {
if (m_sshTerminal != nullptr) {
m_sshTerminal->kill();
}
setState(SessionState::Disconnected, QStringLiteral("Session disconnected."));
return;
}
emit requestDisconnect();
}
void SessionTab::reconnectSession()
{
if (!validateProfileForConnect()) {
return;
}
const std::optional<SessionConnectOptions> options = buildConnectOptions();
if (!options.has_value()) {
return;
}
m_lastConnectOptions = options.value();
if (m_useKodoTermForSsh) {
if (m_sshTerminal != nullptr) {
m_sshTerminal->kill();
}
QTimer::singleShot(50,
this,
[this, options]() { startSshTerminal(options.value()); });
return;
}
emit requestReconnect(options.value());
}
void SessionTab::clearTerminal()
{
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
m_sshTerminal->clearScrollback();
m_sshTerminal->setFocus();
return;
}
if (m_terminalOutput != nullptr) {
m_terminalOutput->clear();
if (m_state == SessionState::Connected) {
emit requestInput(QStringLiteral("\x0c"));
}
m_terminalOutput->setFocus();
}
}
void SessionTab::setTerminalThemeName(const QString& themeName)
{
const QString normalized = themeName.trimmed();
if (normalized.isEmpty()) {
return;
}
if (m_terminalThemeName.compare(normalized, Qt::CaseInsensitive) == 0) {
return;
}
m_terminalThemeName = normalized;
applyTerminalTheme(m_terminalThemeName);
appendEvent(QStringLiteral("Terminal theme set to %1.").arg(m_terminalThemeName));
}
QString SessionTab::terminalThemeName() const
{
return m_terminalThemeName;
}
void SessionTab::onBackendStateChanged(SessionState state, const QString& message)
{
setState(state, message);
}
void SessionTab::onBackendEventLogged(const QString& message)
{
appendEvent(message);
}
void SessionTab::onBackendConnectionError(const QString& displayMessage, const QString& rawMessage)
{
m_lastError = rawMessage.isEmpty() ? displayMessage : rawMessage;
appendEvent(QStringLiteral("Error: %1").arg(displayMessage));
}
void SessionTab::onBackendOutputReceived(const QString& text)
{
if (text.isEmpty() || m_terminalOutput == nullptr) {
return;
}
m_terminalOutput->appendTerminalData(text);
}
void SessionTab::onBackendHostKeyConfirmationRequested(const QString& prompt)
{
const QString question = prompt.isEmpty()
? QStringLiteral("Unknown SSH host key. Do you trust this host?")
: prompt;
const QMessageBox::StandardButton reply = QMessageBox::question(
this,
QStringLiteral("SSH Host Key Confirmation"),
QStringLiteral("%1\n\nTrust and continue?").arg(question),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No);
emit requestHostKeyConfirmation(reply == QMessageBox::Yes);
}
void SessionTab::setupUi()
{
auto* rootLayout = new QVBoxLayout(this);
if (m_useKodoTermForSsh) {
m_sshTerminal = new KodoTerm(this);
const QFont terminalFont = defaultTerminalFont();
KodoTermConfig config = m_sshTerminal->getConfig();
config.font = terminalFont;
config.textAntialiasing = true;
config.maxScrollback = 12000;
m_sshTerminal->setConfig(config);
rootLayout->addWidget(m_sshTerminal, 1);
} else {
m_terminalOutput = new TerminalView(this);
m_terminalOutput->setFont(defaultTerminalFont());
m_terminalOutput->setMinimumHeight(260);
m_terminalOutput->setPlaceholderText(
QStringLiteral("Session is connecting. Type directly here once connected."));
rootLayout->addWidget(m_terminalOutput, 1);
}
applyTerminalTheme(m_terminalThemeName);
auto* eventsHeader = new QHBoxLayout();
m_toggleEventsButton = new QToolButton(this);
m_toggleEventsButton->setCheckable(true);
eventsHeader->addWidget(m_toggleEventsButton);
eventsHeader->addStretch();
m_eventsPanel = new QWidget(this);
auto* eventsLayout = new QVBoxLayout(m_eventsPanel);
eventsLayout->setContentsMargins(0, 0, 0, 0);
auto* eventTitle = new QLabel(QStringLiteral("Session Events"), m_eventsPanel);
m_eventLog = new QPlainTextEdit(m_eventsPanel);
m_eventLog->setReadOnly(true);
m_eventLog->setPlaceholderText(QStringLiteral("Session event log..."));
m_eventLog->setMinimumHeight(140);
eventsLayout->addWidget(eventTitle);
eventsLayout->addWidget(m_eventLog);
rootLayout->addLayout(eventsHeader);
rootLayout->addWidget(m_eventsPanel);
setPanelExpanded(m_toggleEventsButton, m_eventsPanel, QStringLiteral("Events"), false);
connect(m_toggleEventsButton,
&QToolButton::toggled,
this,
[this](bool expanded) {
setPanelExpanded(
m_toggleEventsButton, m_eventsPanel, QStringLiteral("Events"), expanded);
});
if (m_terminalOutput != nullptr) {
connect(m_terminalOutput,
&TerminalView::inputGenerated,
this,
[this](const QString& input) { emit requestInput(input); });
connect(m_terminalOutput,
&TerminalView::terminalSizeChanged,
this,
[this](int columns, int rows) { emit requestTerminalSize(columns, rows); });
}
}
std::optional<SessionConnectOptions> SessionTab::buildConnectOptions()
{
SessionConnectOptions options;
options.knownHostsPolicy = m_profile.knownHostsPolicy;
if (m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) {
return options;
}
if (m_useKodoTermForSsh
&& m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) {
// Password is entered directly in terminal prompt.
return options;
}
if (m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) {
bool accepted = false;
const QString password = QInputDialog::getText(this,
QStringLiteral("SSH Password"),
QStringLiteral("Password for %1@%2:")
.arg(m_profile.username, m_profile.host),
QLineEdit::Password,
QString(),
&accepted);
if (!accepted) {
return std::nullopt;
}
if (password.isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("Password is required for password authentication."));
return std::nullopt;
}
options.password = password;
return options;
}
QString keyPath = m_profile.privateKeyPath.trimmed();
if (keyPath.isEmpty()) {
keyPath = QFileDialog::getOpenFileName(this,
QStringLiteral("Select Private Key"),
QString(),
QStringLiteral("All Files (*)"));
if (keyPath.isEmpty()) {
return std::nullopt;
}
}
if (!QFileInfo::exists(keyPath)) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("Private key file not found: %1").arg(keyPath));
return std::nullopt;
}
options.privateKeyPath = keyPath;
return options;
}
bool SessionTab::validateProfileForConnect()
{
if (m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) != 0) {
return true;
}
if (m_profile.host.trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("SSH host is required."));
return false;
}
if (m_profile.username.trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("SSH username is required."));
return false;
}
if (m_profile.port < 1 || m_profile.port > 65535) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("SSH port must be between 1 and 65535."));
return false;
}
return true;
}
void SessionTab::appendEvent(const QString& message)
{
const QString timestamp = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm:ss"));
m_eventLog->appendPlainText(QStringLiteral("[%1] %2").arg(timestamp, message));
}
void SessionTab::setState(SessionState state, const QString& message)
{
m_state = state;
appendEvent(QStringLiteral("Connection state: %1").arg(message));
refreshActionButtons();
emit tabTitleChanged(tabTitle());
emit tabStateChanged(state);
}
QString SessionTab::stateSuffix() const
{
switch (m_state) {
case SessionState::Disconnected:
return QStringLiteral("Disconnected");
case SessionState::Connecting:
return QStringLiteral("Connecting");
case SessionState::Connected:
return QStringLiteral("Connected");
case SessionState::Failed:
return QStringLiteral("Failed");
}
return QStringLiteral("Unknown");
}
void SessionTab::refreshActionButtons()
{
const bool isConnected = m_state == SessionState::Connected;
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
m_sshTerminal->setEnabled(true);
m_sshTerminal->setFocus();
return;
}
if (m_terminalOutput != nullptr) {
m_terminalOutput->setEnabled(isConnected);
m_terminalOutput->setFocus();
}
}
void SessionTab::setPanelExpanded(QToolButton* button,
QWidget* panel,
const QString& name,
bool expanded)
{
if (button == nullptr || panel == nullptr) {
return;
}
button->blockSignals(true);
button->setChecked(expanded);
button->blockSignals(false);
panel->setVisible(expanded);
button->setText(expanded ? QStringLiteral("Hide %1").arg(name)
: QStringLiteral("Show %1").arg(name));
}
bool SessionTab::startSshTerminal(const SessionConnectOptions& options)
{
if (m_sshTerminal == nullptr) {
return false;
}
QStringList args;
args << QStringLiteral("-tt") << QStringLiteral("-p") << QString::number(m_profile.port)
<< QStringLiteral("-o") << QStringLiteral("ConnectTimeout=12") << QStringLiteral("-o")
<< QStringLiteral("ServerAliveInterval=20") << QStringLiteral("-o")
<< QStringLiteral("ServerAliveCountMax=2");
const QString policy = options.knownHostsPolicy.trimmed().isEmpty()
? m_profile.knownHostsPolicy.trimmed()
: options.knownHostsPolicy.trimmed();
if (policy.compare(QStringLiteral("Ignore"), Qt::CaseInsensitive) == 0) {
#ifdef Q_OS_WIN
const QString knownHostsNullDevice = QStringLiteral("NUL");
#else
const QString knownHostsNullDevice = QStringLiteral("/dev/null");
#endif
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=no")
<< QStringLiteral("-o")
<< QStringLiteral("UserKnownHostsFile=%1").arg(knownHostsNullDevice);
} else if (policy.compare(QStringLiteral("Accept New"), Qt::CaseInsensitive) == 0) {
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=accept-new");
} else if (policy.compare(QStringLiteral("Ask"), Qt::CaseInsensitive) == 0) {
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=ask");
} else {
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=yes");
}
if (m_profile.authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) {
QString keyPath = options.privateKeyPath.trimmed();
if (keyPath.isEmpty()) {
keyPath = m_profile.privateKeyPath.trimmed();
}
if (keyPath.isEmpty()) {
m_lastError = QStringLiteral("Private key path is required.");
appendEvent(QStringLiteral("Error: %1").arg(m_lastError));
setState(SessionState::Failed, m_lastError);
return false;
}
args << QStringLiteral("-i") << keyPath;
}
const QString target = m_profile.username.trimmed().isEmpty()
? m_profile.host.trimmed()
: QStringLiteral("%1@%2").arg(m_profile.username.trimmed(), m_profile.host.trimmed());
args << target;
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
if (!env.contains(QStringLiteral("TERM"))) {
env.insert(QStringLiteral("TERM"), QStringLiteral("xterm-256color"));
}
if (!env.contains(QStringLiteral("COLORTERM"))) {
env.insert(QStringLiteral("COLORTERM"), QStringLiteral("truecolor"));
}
m_sshTerminal->setProgram(QStringLiteral("ssh"));
m_sshTerminal->setArguments(args);
m_sshTerminal->setProcessEnvironment(env);
appendEvent(QStringLiteral("Launching SSH terminal session."));
setState(SessionState::Connecting, QStringLiteral("Starting SSH terminal..."));
if (!m_sshTerminal->start()) {
m_lastError = QStringLiteral("Failed to start embedded SSH terminal process.");
appendEvent(QStringLiteral("Error: %1").arg(m_lastError));
setState(SessionState::Failed, QStringLiteral("Failed to start SSH terminal."));
return false;
}
setState(SessionState::Connected, QStringLiteral("SSH session established."));
return true;
}
void SessionTab::applyTerminalTheme(const QString& themeName)
{
if (m_useKodoTermForSsh) {
if (m_sshTerminal != nullptr) {
m_sshTerminal->setTheme(themeForName(themeName));
}
return;
}
if (m_terminalOutput != nullptr) {
m_terminalOutput->setThemeName(themeName);
}
}

79
src/session_tab.h Normal file
View File

@@ -0,0 +1,79 @@
#ifndef ORBITHUB_SESSION_TAB_H
#define ORBITHUB_SESSION_TAB_H
#include "profile_repository.h"
#include "session_backend.h"
#include <QWidget>
#include <optional>
class QPlainTextEdit;
class QThread;
class SessionBackend;
class TerminalView;
class QToolButton;
class KodoTerm;
class SessionTab : public QWidget
{
Q_OBJECT
public:
explicit SessionTab(const Profile& profile, QWidget* parent = nullptr);
~SessionTab() override;
QString tabTitle() const;
void connectSession();
void disconnectSession();
void reconnectSession();
void clearTerminal();
void setTerminalThemeName(const QString& themeName);
QString terminalThemeName() const;
signals:
void tabTitleChanged(const QString& title);
void tabStateChanged(SessionState state);
void requestConnect(const SessionConnectOptions& options);
void requestDisconnect();
void requestReconnect(const SessionConnectOptions& options);
void requestInput(const QString& input);
void requestHostKeyConfirmation(bool trustHost);
void requestTerminalSize(int columns, int rows);
private slots:
void onBackendStateChanged(SessionState state, const QString& message);
void onBackendEventLogged(const QString& message);
void onBackendConnectionError(const QString& displayMessage, const QString& rawMessage);
void onBackendOutputReceived(const QString& text);
void onBackendHostKeyConfirmationRequested(const QString& prompt);
private:
Profile m_profile;
QThread* m_backendThread;
SessionBackend* m_backend;
bool m_useKodoTermForSsh;
SessionState m_state;
QString m_lastError;
SessionConnectOptions m_lastConnectOptions;
QString m_terminalThemeName;
KodoTerm* m_sshTerminal;
TerminalView* m_terminalOutput;
QPlainTextEdit* m_eventLog;
QToolButton* m_toggleEventsButton;
QWidget* m_eventsPanel;
void setupUi();
std::optional<SessionConnectOptions> buildConnectOptions();
bool validateProfileForConnect();
void appendEvent(const QString& message);
void setState(SessionState state, const QString& message);
QString stateSuffix() const;
void refreshActionButtons();
void setPanelExpanded(QToolButton* button, QWidget* panel, const QString& name, bool expanded);
bool startSshTerminal(const SessionConnectOptions& options);
void applyTerminalTheme(const QString& themeName);
};
#endif

View File

@@ -1,45 +1,142 @@
#include "session_window.h" #include "session_window.h"
#include <QFont> #include "session_tab.h"
#include <QLabel>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QWidget>
SessionWindow::SessionWindow(const QString& profileName, QWidget* parent) #include <QAction>
#include <QColor>
#include <QMenu>
#include <QPalette>
#include <QStringList>
#include <QTabBar>
#include <QTabWidget>
namespace {
QColor tabColorForState(SessionState state, const QPalette& palette)
{
switch (state) {
case SessionState::Disconnected:
return palette.color(QPalette::WindowText);
case SessionState::Connecting:
return QColor(QStringLiteral("#9a6700"));
case SessionState::Connected:
return QColor(QStringLiteral("#2e7d32"));
case SessionState::Failed:
return QColor(QStringLiteral("#c62828"));
}
return palette.color(QPalette::WindowText);
}
QStringList terminalThemeNames()
{
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
}
}
SessionWindow::SessionWindow(const Profile& profile, QWidget* parent)
: QMainWindow(parent), m_tabs(new QTabWidget(this)) : QMainWindow(parent), m_tabs(new QTabWidget(this))
{ {
setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profileName)); setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name));
resize(900, 600); resize(1080, 760);
m_tabs->setTabsClosable(true);
connect(m_tabs,
&QTabWidget::tabCloseRequested,
this,
[this](int index) {
QWidget* tab = m_tabs->widget(index);
if (auto* sessionTab = qobject_cast<SessionTab*>(tab)) {
sessionTab->disconnectSession();
}
m_tabs->removeTab(index);
delete tab;
if (m_tabs->count() == 0) {
close();
}
});
m_tabs->tabBar()->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_tabs->tabBar(),
&QWidget::customContextMenuRequested,
this,
[this](const QPoint& pos) {
const int index = m_tabs->tabBar()->tabAt(pos);
if (index < 0) {
return;
}
auto* tab = qobject_cast<SessionTab*>(m_tabs->widget(index));
if (tab == nullptr) {
return;
}
QMenu menu(this);
QAction* disconnectAction = menu.addAction(QStringLiteral("Disconnect"));
QAction* reconnectAction = menu.addAction(QStringLiteral("Reconnect"));
menu.addSeparator();
QMenu* themeMenu = menu.addMenu(QStringLiteral("Theme"));
QList<QAction*> themeActions;
const QString currentTheme = tab->terminalThemeName();
for (const QString& themeName : terminalThemeNames()) {
QAction* themeAction = themeMenu->addAction(themeName);
themeAction->setCheckable(true);
themeAction->setChecked(
themeName.compare(currentTheme, Qt::CaseInsensitive) == 0);
themeActions.append(themeAction);
}
QAction* clearAction = menu.addAction(QStringLiteral("Clear"));
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
if (chosen == disconnectAction) {
tab->disconnectSession();
} else if (chosen == reconnectAction) {
tab->reconnectSession();
} else if (chosen == clearAction) {
tab->clearTerminal();
} else {
for (QAction* themeAction : themeActions) {
if (chosen == themeAction) {
tab->setTerminalThemeName(themeAction->text());
break;
}
}
}
});
setCentralWidget(m_tabs); setCentralWidget(m_tabs);
addPlaceholderTab(profileName); addSessionTab(profile);
} }
void SessionWindow::addPlaceholderTab(const QString& profileName) void SessionWindow::addSessionTab(const Profile& profile)
{ {
auto* container = new QWidget(this); auto* tab = new SessionTab(profile, this);
auto* layout = new QVBoxLayout(container); const int index = m_tabs->addTab(tab, tab->tabTitle());
m_tabs->setCurrentIndex(index);
m_tabs->tabBar()->setTabTextColor(
index, tabColorForState(SessionState::Disconnected, m_tabs->palette()));
auto* titleLabel = new QLabel(QStringLiteral("Profile: %1").arg(profileName), container); connect(tab,
auto* surfaceLabel = new QLabel(QStringLiteral("OrbitHub Native Surface"), container); &SessionTab::tabTitleChanged,
this,
QFont titleFont = titleLabel->font(); [this, tab](const QString& title) { updateTabTitle(tab, title); });
titleFont.setBold(true); connect(tab,
titleLabel->setFont(titleFont); &SessionTab::tabStateChanged,
this,
QFont surfaceFont = surfaceLabel->font(); [this, tab](SessionState state) {
surfaceFont.setPointSize(surfaceFont.pointSize() + 6); for (int i = 0; i < m_tabs->count(); ++i) {
surfaceFont.setBold(true); if (m_tabs->widget(i) == tab) {
surfaceLabel->setFont(surfaceFont); m_tabs->tabBar()->setTabTextColor(
i, tabColorForState(state, m_tabs->palette()));
surfaceLabel->setAlignment(Qt::AlignCenter); return;
surfaceLabel->setMinimumHeight(200); }
surfaceLabel->setStyleSheet( }
QStringLiteral("border: 1px solid #8a8a8a; background-color: #f5f5f5;")); });
}
layout->addWidget(titleLabel);
layout->addWidget(surfaceLabel, 1); void SessionWindow::updateTabTitle(SessionTab* tab, const QString& title)
{
m_tabs->addTab(container, profileName); for (int i = 0; i < m_tabs->count(); ++i) {
if (m_tabs->widget(i) == tab) {
m_tabs->setTabText(i, title);
return;
}
}
} }

View File

@@ -1,22 +1,25 @@
#ifndef ORBITHUB_SESSION_WINDOW_H #ifndef ORBITHUB_SESSION_WINDOW_H
#define ORBITHUB_SESSION_WINDOW_H #define ORBITHUB_SESSION_WINDOW_H
#include "profile_repository.h"
#include <QMainWindow> #include <QMainWindow>
#include <QString>
class QTabWidget; class QTabWidget;
class SessionTab;
class SessionWindow : public QMainWindow class SessionWindow : public QMainWindow
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit SessionWindow(const QString& profileName, QWidget* parent = nullptr); explicit SessionWindow(const Profile& profile, QWidget* parent = nullptr);
private: private:
QTabWidget* m_tabs; QTabWidget* m_tabs;
void addPlaceholderTab(const QString& profileName); void addSessionTab(const Profile& profile);
void updateTabTitle(SessionTab* tab, const QString& title);
}; };
#endif #endif

538
src/ssh_session_backend.cpp Normal file
View File

@@ -0,0 +1,538 @@
#include "ssh_session_backend.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QProcessEnvironment>
#include <QTextStream>
#include <QUuid>
namespace {
QString escapeForShellSingleQuotes(const QString& value)
{
QString escaped = value;
escaped.replace(QStringLiteral("'"), QStringLiteral("'\"'\"'"));
return escaped;
}
}
SshSessionBackend::SshSessionBackend(const Profile& profile, QObject* parent)
: SessionBackend(profile, parent),
m_process(new QProcess(this)),
m_connectedProbeTimer(new QTimer(this)),
m_state(SessionState::Disconnected),
m_userInitiatedDisconnect(false),
m_reconnectPending(false),
m_waitingForPasswordPrompt(false),
m_waitingForHostKeyConfirmation(false),
m_passwordSubmitted(false),
m_terminalColumns(0),
m_terminalRows(0)
{
m_connectedProbeTimer->setSingleShot(true);
connect(m_process, &QProcess::started, this, &SshSessionBackend::onProcessStarted);
connect(m_process,
&QProcess::errorOccurred,
this,
&SshSessionBackend::onProcessErrorOccurred);
connect(m_process,
qOverload<int, QProcess::ExitStatus>(&QProcess::finished),
this,
&SshSessionBackend::onProcessFinished);
connect(m_process,
&QProcess::readyReadStandardOutput,
this,
&SshSessionBackend::onReadyReadStandardOutput);
connect(m_process,
&QProcess::readyReadStandardError,
this,
&SshSessionBackend::onReadyReadStandardError);
connect(m_connectedProbeTimer,
&QTimer::timeout,
this,
&SshSessionBackend::onConnectedProbeTimeout);
}
SshSessionBackend::~SshSessionBackend()
{
if (m_process->state() != QProcess::NotRunning) {
m_process->kill();
m_process->waitForFinished(500);
}
cleanupAskPassScript();
}
void SshSessionBackend::connectSession(const SessionConnectOptions& options)
{
if (m_state == SessionState::Connected || m_state == SessionState::Connecting) {
emit eventLogged(QStringLiteral("Connect skipped: session is already active."));
return;
}
m_userInitiatedDisconnect = false;
m_reconnectPending = false;
m_lastRawError.clear();
m_activeOptions = options;
m_waitingForPasswordPrompt = false;
m_waitingForHostKeyConfirmation = false;
m_passwordSubmitted = false;
if (!startSshProcess(options)) {
return;
}
setState(SessionState::Connecting, QStringLiteral("Connecting to SSH endpoint..."));
emit eventLogged(QStringLiteral("Launching ssh client."));
}
void SshSessionBackend::disconnectSession()
{
if (m_process->state() == QProcess::NotRunning) {
if (m_state != SessionState::Disconnected) {
setState(SessionState::Disconnected, QStringLiteral("Session is disconnected."));
}
return;
}
m_userInitiatedDisconnect = true;
emit eventLogged(QStringLiteral("Disconnect requested."));
m_connectedProbeTimer->stop();
m_process->terminate();
QTimer::singleShot(1500,
this,
[this]() {
if (m_process->state() != QProcess::NotRunning) {
emit eventLogged(QStringLiteral("Force-stopping ssh process."));
m_process->kill();
}
});
}
void SshSessionBackend::reconnectSession(const SessionConnectOptions& options)
{
emit eventLogged(QStringLiteral("Reconnect requested."));
if (m_process->state() == QProcess::NotRunning) {
connectSession(options);
return;
}
m_reconnectPending = true;
m_reconnectOptions = options;
m_userInitiatedDisconnect = true;
m_process->terminate();
}
void SshSessionBackend::sendInput(const QString& input)
{
if (m_process->state() != QProcess::Running) {
emit eventLogged(QStringLiteral("Input ignored: session is not running."));
return;
}
if (input.isEmpty()) {
return;
}
m_process->write(input.toUtf8());
}
void SshSessionBackend::confirmHostKey(bool trustHost)
{
if (m_process->state() != QProcess::Running || !m_waitingForHostKeyConfirmation) {
return;
}
m_waitingForHostKeyConfirmation = false;
const QString response = trustHost ? QStringLiteral("yes\n") : QStringLiteral("no\n");
m_process->write(response.toUtf8());
emit eventLogged(trustHost
? QStringLiteral("Host key accepted by user.")
: QStringLiteral("Host key rejected by user."));
}
void SshSessionBackend::updateTerminalSize(int columns, int rows)
{
m_terminalColumns = columns;
m_terminalRows = rows;
if (m_state == SessionState::Connected) {
applyTerminalSizeIfAvailable();
}
}
void SshSessionBackend::onProcessStarted()
{
emit eventLogged(QStringLiteral("ssh process started."));
m_connectedProbeTimer->start(1200);
}
void SshSessionBackend::onProcessErrorOccurred(QProcess::ProcessError)
{
const QString rawError = m_process->errorString();
if (!rawError.isEmpty()) {
m_lastRawError += rawError + QLatin1Char('\n');
}
if (m_state == SessionState::Connecting) {
const QString display = mapSshError(m_lastRawError);
setState(SessionState::Failed, display);
emit connectionError(display, m_lastRawError.trimmed());
}
}
void SshSessionBackend::onProcessFinished(int exitCode, QProcess::ExitStatus)
{
m_connectedProbeTimer->stop();
cleanupAskPassScript();
if (m_reconnectPending) {
m_reconnectPending = false;
SessionConnectOptions options = m_reconnectOptions;
setState(SessionState::Disconnected, QStringLiteral("Reconnecting..."));
QTimer::singleShot(0, this, [this, options]() { connectSession(options); });
return;
}
if (m_userInitiatedDisconnect) {
m_userInitiatedDisconnect = false;
setState(SessionState::Disconnected, QStringLiteral("Session disconnected."));
emit eventLogged(QStringLiteral("ssh process exited after disconnect request."));
return;
}
if (m_state == SessionState::Connecting || exitCode != 0) {
QString rawError = m_lastRawError.trimmed();
if (rawError.isEmpty()) {
rawError = QStringLiteral("ssh exited with code %1").arg(exitCode);
}
const QString display = mapSshError(rawError);
setState(SessionState::Failed, display);
emit connectionError(display, rawError);
return;
}
setState(SessionState::Disconnected, QStringLiteral("SSH session ended."));
}
void SshSessionBackend::onReadyReadStandardOutput()
{
const QString chunk = QString::fromUtf8(m_process->readAllStandardOutput());
if (chunk.isEmpty()) {
return;
}
emit outputReceived(chunk);
if (m_state == SessionState::Connecting && !m_waitingForHostKeyConfirmation
&& !m_waitingForPasswordPrompt) {
setState(SessionState::Connected, QStringLiteral("SSH session established."));
}
}
void SshSessionBackend::onReadyReadStandardError()
{
const QString chunk = QString::fromUtf8(m_process->readAllStandardError());
if (chunk.isEmpty()) {
return;
}
m_lastRawError += chunk;
emit outputReceived(chunk);
const QStringList lines = chunk.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
for (const QString& line : lines) {
const QString trimmed = line.trimmed();
if (!trimmed.isEmpty()) {
emit eventLogged(trimmed);
}
if (trimmed.contains(QStringLiteral("Are you sure you want to continue connecting"),
Qt::CaseInsensitive)
&& !m_waitingForHostKeyConfirmation) {
m_waitingForHostKeyConfirmation = true;
emit eventLogged(QStringLiteral("Awaiting host key confirmation from user."));
emit hostKeyConfirmationRequested(trimmed);
continue;
}
if (trimmed.contains(QStringLiteral("password:"), Qt::CaseInsensitive)
&& profile().authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0
&& !m_passwordSubmitted) {
if (m_activeOptions.password.isEmpty()) {
const QString message = QStringLiteral("Password prompt received but no password is available.");
setState(SessionState::Failed, message);
emit connectionError(message, trimmed);
return;
}
m_waitingForPasswordPrompt = false;
m_passwordSubmitted = true;
m_process->write((m_activeOptions.password + QStringLiteral("\n")).toUtf8());
emit eventLogged(QStringLiteral("Password prompt received; credentials submitted."));
continue;
}
}
}
void SshSessionBackend::onConnectedProbeTimeout()
{
if (m_state != SessionState::Connecting) {
return;
}
if (m_process->state() == QProcess::Running && !m_waitingForHostKeyConfirmation
&& !m_waitingForPasswordPrompt) {
setState(SessionState::Connected, QStringLiteral("SSH session established."));
}
}
void SshSessionBackend::setState(SessionState state, const QString& message)
{
m_state = state;
emit stateChanged(state, message);
emit eventLogged(message);
if (m_state == SessionState::Connected) {
applyTerminalSizeIfAvailable();
}
}
bool SshSessionBackend::startSshProcess(const SessionConnectOptions& options)
{
const Profile& p = profile();
if (p.host.trimmed().isEmpty()) {
const QString message = QStringLiteral("Host is required for SSH connections.");
setState(SessionState::Failed, message);
emit connectionError(message, message);
return false;
}
if (p.port < 1 || p.port > 65535) {
const QString message = QStringLiteral("Port must be between 1 and 65535.");
setState(SessionState::Failed, message);
emit connectionError(message, message);
return false;
}
QStringList args;
args << QStringLiteral("-tt") << QStringLiteral("-p") << QString::number(p.port)
<< QStringLiteral("-o") << QStringLiteral("ConnectTimeout=12") << QStringLiteral("-o")
<< QStringLiteral("ServerAliveInterval=20") << QStringLiteral("-o")
<< QStringLiteral("ServerAliveCountMax=2");
const QString policy = options.knownHostsPolicy.trimmed().isEmpty()
? p.knownHostsPolicy.trimmed()
: options.knownHostsPolicy.trimmed();
if (policy.compare(QStringLiteral("Ignore"), Qt::CaseInsensitive) == 0) {
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=no")
<< QStringLiteral("-o")
<< QStringLiteral("UserKnownHostsFile=%1").arg(knownHostsFileForNullDevice());
} else if (policy.compare(QStringLiteral("Accept New"), Qt::CaseInsensitive) == 0) {
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=accept-new");
} else if (policy.compare(QStringLiteral("Ask"), Qt::CaseInsensitive) == 0) {
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=ask");
} else {
args << QStringLiteral("-o") << QStringLiteral("StrictHostKeyChecking=yes");
}
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
if (p.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) {
if (options.password.isEmpty()) {
const QString message = QStringLiteral("Password is required for password authentication.");
setState(SessionState::Failed, message);
emit connectionError(message, message);
return false;
}
args << QStringLiteral("-o") << QStringLiteral("PreferredAuthentications=password")
<< QStringLiteral("-o") << QStringLiteral("PubkeyAuthentication=no")
<< QStringLiteral("-o") << QStringLiteral("NumberOfPasswordPrompts=1");
m_waitingForPasswordPrompt = false;
QString askPassError;
if (!configureAskPass(options, environment, askPassError)) {
setState(SessionState::Failed, askPassError);
emit connectionError(askPassError, askPassError);
return false;
}
} else if (p.authMode.compare(QStringLiteral("Private Key"), Qt::CaseInsensitive) == 0) {
QString keyPath = options.privateKeyPath.trimmed();
if (keyPath.isEmpty()) {
keyPath = p.privateKeyPath.trimmed();
}
if (keyPath.isEmpty()) {
const QString message = QStringLiteral("Private key path is required.");
setState(SessionState::Failed, message);
emit connectionError(message, message);
return false;
}
if (!QFileInfo::exists(keyPath)) {
const QString message = QStringLiteral("Private key file does not exist: %1")
.arg(keyPath);
setState(SessionState::Failed, message);
emit connectionError(message, message);
return false;
}
args << QStringLiteral("-i") << keyPath << QStringLiteral("-o")
<< QStringLiteral("PreferredAuthentications=publickey") << QStringLiteral("-o")
<< QStringLiteral("PasswordAuthentication=no");
}
const QString target = p.username.trimmed().isEmpty()
? p.host.trimmed()
: QStringLiteral("%1@%2").arg(p.username.trimmed(), p.host.trimmed());
args << target;
m_process->setProcessEnvironment(environment);
m_process->setProgram(QStringLiteral("ssh"));
m_process->setArguments(args);
m_process->setProcessChannelMode(QProcess::SeparateChannels);
m_process->start();
if (!m_process->waitForStarted(3000)) {
const QString rawError = m_process->errorString();
const QString display = mapSshError(rawError);
setState(SessionState::Failed, display);
emit connectionError(display, rawError);
return false;
}
return true;
}
bool SshSessionBackend::configureAskPass(const SessionConnectOptions& options,
QProcessEnvironment& environment,
QString& error)
{
cleanupAskPassScript();
#ifdef Q_OS_WIN
m_askPassScriptPath = QDir::temp().filePath(
QStringLiteral("orbithub_askpass_%1.cmd")
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces)));
#else
m_askPassScriptPath = QDir::temp().filePath(
QStringLiteral("orbithub_askpass_%1.sh")
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces)));
#endif
QFile script(m_askPassScriptPath);
if (!script.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
error = QStringLiteral("Failed to create temporary askpass helper script.");
cleanupAskPassScript();
return false;
}
QTextStream out(&script);
#ifdef Q_OS_WIN
out << "@echo off\r\n";
out << "echo " << options.password << "\r\n";
#else
const QString escapedPassword = escapeForShellSingleQuotes(options.password);
out << "#!/bin/sh\n";
out << "printf '%s\\n' '" << escapedPassword << "'\n";
#endif
out.flush();
script.close();
#ifndef Q_OS_WIN
if (!QFile::setPermissions(m_askPassScriptPath,
QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)) {
error = QStringLiteral("Failed to set permissions on askpass helper script.");
cleanupAskPassScript();
return false;
}
#endif
environment.insert(QStringLiteral("SSH_ASKPASS"), m_askPassScriptPath);
environment.insert(QStringLiteral("SSH_ASKPASS_REQUIRE"), QStringLiteral("force"));
if (!environment.contains(QStringLiteral("DISPLAY"))) {
environment.insert(QStringLiteral("DISPLAY"), QStringLiteral(":0"));
}
return true;
}
void SshSessionBackend::cleanupAskPassScript()
{
if (!m_askPassScriptPath.isEmpty()) {
QFile::remove(m_askPassScriptPath);
m_askPassScriptPath.clear();
}
}
QString SshSessionBackend::mapSshError(const QString& rawError) const
{
const QString raw = rawError.trimmed();
if (raw.contains(QStringLiteral("Permission denied"), Qt::CaseInsensitive)) {
return QStringLiteral("Authentication failed. Check username and credentials.");
}
if (raw.contains(QStringLiteral("Host key verification failed"), Qt::CaseInsensitive)) {
return QStringLiteral("Host key verification failed.");
}
if (raw.contains(QStringLiteral("Could not resolve hostname"), Qt::CaseInsensitive)) {
return QStringLiteral("Host could not be resolved.");
}
if (raw.contains(QStringLiteral("Connection timed out"), Qt::CaseInsensitive)
|| raw.contains(QStringLiteral("Operation timed out"), Qt::CaseInsensitive)) {
return QStringLiteral("Connection timed out.");
}
if (raw.contains(QStringLiteral("Connection refused"), Qt::CaseInsensitive)) {
return QStringLiteral("Connection refused by remote host.");
}
if (raw.contains(QStringLiteral("No route to host"), Qt::CaseInsensitive)) {
return QStringLiteral("No route to host.");
}
if (raw.contains(QStringLiteral("Identity file"), Qt::CaseInsensitive)
&& raw.contains(QStringLiteral("not accessible"), Qt::CaseInsensitive)) {
return QStringLiteral("Private key file is not accessible.");
}
if (raw.contains(QStringLiteral("No such file or directory"), Qt::CaseInsensitive)) {
if (raw.contains(QStringLiteral("ssh-askpass"), Qt::CaseInsensitive)) {
return QStringLiteral("SSH password helper is missing or failed to launch.");
}
return QStringLiteral("Required file was not found.");
}
if (raw.isEmpty()) {
return QStringLiteral("SSH connection failed for an unknown reason.");
}
return QStringLiteral("SSH connection failed.");
}
QString SshSessionBackend::knownHostsFileForNullDevice() const
{
#ifdef Q_OS_WIN
return QStringLiteral("NUL");
#else
return QStringLiteral("/dev/null");
#endif
}
void SshSessionBackend::applyTerminalSizeIfAvailable()
{
if (m_process->state() != QProcess::Running) {
return;
}
if (m_terminalColumns <= 0 || m_terminalRows <= 0) {
return;
}
const QString command = QStringLiteral("stty cols %1 rows %2\\n")
.arg(m_terminalColumns)
.arg(m_terminalRows);
m_process->write(command.toUtf8());
emit eventLogged(
QStringLiteral("Applied terminal size: %1x%2").arg(m_terminalColumns).arg(m_terminalRows));
}

61
src/ssh_session_backend.h Normal file
View File

@@ -0,0 +1,61 @@
#ifndef ORBITHUB_SSH_SESSION_BACKEND_H
#define ORBITHUB_SSH_SESSION_BACKEND_H
#include "session_backend.h"
#include <QProcess>
#include <QString>
#include <QTimer>
class SshSessionBackend : public SessionBackend
{
Q_OBJECT
public:
explicit SshSessionBackend(const Profile& profile, QObject* parent = nullptr);
~SshSessionBackend() override;
public slots:
void connectSession(const SessionConnectOptions& options) override;
void disconnectSession() override;
void reconnectSession(const SessionConnectOptions& options) override;
void sendInput(const QString& input) override;
void confirmHostKey(bool trustHost) override;
void updateTerminalSize(int columns, int rows) override;
private slots:
void onProcessStarted();
void onProcessErrorOccurred(QProcess::ProcessError error);
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
void onReadyReadStandardOutput();
void onReadyReadStandardError();
void onConnectedProbeTimeout();
private:
QProcess* m_process;
QTimer* m_connectedProbeTimer;
SessionState m_state;
bool m_userInitiatedDisconnect;
bool m_reconnectPending;
SessionConnectOptions m_reconnectOptions;
SessionConnectOptions m_activeOptions;
QString m_lastRawError;
QString m_askPassScriptPath;
bool m_waitingForPasswordPrompt;
bool m_waitingForHostKeyConfirmation;
bool m_passwordSubmitted;
int m_terminalColumns;
int m_terminalRows;
void setState(SessionState state, const QString& message);
bool startSshProcess(const SessionConnectOptions& options);
bool configureAskPass(const SessionConnectOptions& options,
QProcessEnvironment& environment,
QString& error);
void cleanupAskPassScript();
QString mapSshError(const QString& rawError) const;
QString knownHostsFileForNullDevice() const;
void applyTerminalSizeIfAvailable();
};
#endif

520
src/terminal_view.cpp Normal file
View File

@@ -0,0 +1,520 @@
#include "terminal_view.h"
#include <QApplication>
#include <QClipboard>
#include <QColor>
#include <QFocusEvent>
#include <QFontMetrics>
#include <QKeyEvent>
#include <QResizeEvent>
#include <QTimer>
#include <QTextCursor>
#include <algorithm>
namespace {
QString normalizedThemeName(const QString& value)
{
return value.trimmed().toLower();
}
}
TerminalView::TerminalView(QWidget* parent)
: QTextEdit(parent),
m_bold(false),
m_hasFgColor(false),
m_hasBgColor(false)
{
setReadOnly(false);
setUndoRedoEnabled(false);
setAcceptRichText(false);
setLineWrapMode(QTextEdit::NoWrap);
setContextMenuPolicy(Qt::NoContextMenu);
setCursorWidth(2);
document()->setMaximumBlockCount(4000);
applyThemePalette(paletteByName(QStringLiteral("Dark")));
resetSgrState();
QTimer::singleShot(0, this, [this]() {
moveCursor(QTextCursor::End);
emitTerminalSize();
});
}
QStringList TerminalView::themeNames()
{
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
}
void TerminalView::setThemeName(const QString& themeName)
{
applyThemePalette(paletteByName(themeName));
}
void TerminalView::appendTerminalData(const QString& data)
{
if (data.isEmpty()) {
return;
}
const QString merged = m_pendingEscape + data;
m_pendingEscape.clear();
QString plainBuffer;
for (int i = 0; i < merged.size();) {
const QChar ch = merged.at(i);
if (ch == QChar::fromLatin1('\x1b')) {
if (!plainBuffer.isEmpty()) {
appendTextChunk(plainBuffer);
plainBuffer.clear();
}
if (i + 1 >= merged.size()) {
m_pendingEscape = merged.mid(i);
break;
}
if (merged.at(i + 1) != QChar::fromLatin1('[')) {
i += 2;
continue;
}
int end = i + 2;
while (end < merged.size()) {
const ushort c = merged.at(end).unicode();
if (c >= 0x40 && c <= 0x7e) {
break;
}
++end;
}
if (end >= merged.size()) {
m_pendingEscape = merged.mid(i);
break;
}
const QChar finalByte = merged.at(end);
const QString params = merged.mid(i + 2, end - (i + 2));
if (finalByte == QChar::fromLatin1('m')) {
handleSgrSequence(params);
} else if (finalByte == QChar::fromLatin1('J')) {
if (params.isEmpty() || params == QStringLiteral("2")) {
clear();
}
}
i = end + 1;
continue;
}
if (ch == QChar::fromLatin1('\r')) {
const bool hasLfAfter = (i + 1 < merged.size() && merged.at(i + 1) == QChar::fromLatin1('\n'));
if (!hasLfAfter) {
plainBuffer.append(QChar::fromLatin1('\n'));
}
++i;
continue;
}
plainBuffer.append(ch);
++i;
}
if (!plainBuffer.isEmpty()) {
appendTextChunk(plainBuffer);
}
}
void TerminalView::keyPressEvent(QKeyEvent* event)
{
if (event == nullptr) {
return;
}
moveCursor(QTextCursor::End);
const Qt::KeyboardModifiers modifiers = event->modifiers();
if (modifiers == (Qt::ControlModifier | Qt::ShiftModifier)
&& event->key() == Qt::Key_C) {
const QString selected = textCursor().selectedText();
if (!selected.isEmpty()) {
QApplication::clipboard()->setText(selected);
}
return;
}
if (modifiers == Qt::ControlModifier) {
switch (event->key()) {
case Qt::Key_C:
emit inputGenerated(QStringLiteral("\x03"));
return;
case Qt::Key_D:
emit inputGenerated(QStringLiteral("\x04"));
return;
case Qt::Key_L:
emit inputGenerated(QStringLiteral("\x0c"));
return;
case Qt::Key_V: {
const QString clipboardText = QApplication::clipboard()->text();
if (!clipboardText.isEmpty()) {
emit inputGenerated(clipboardText);
}
return;
}
default:
break;
}
}
switch (event->key()) {
case Qt::Key_Return:
case Qt::Key_Enter:
emit inputGenerated(QStringLiteral("\n"));
return;
case Qt::Key_Backspace:
emit inputGenerated(QStringLiteral("\x7f"));
return;
case Qt::Key_Tab:
emit inputGenerated(QStringLiteral("\t"));
return;
case Qt::Key_Left:
emit inputGenerated(QStringLiteral("\x1b[D"));
return;
case Qt::Key_Right:
emit inputGenerated(QStringLiteral("\x1b[C"));
return;
case Qt::Key_Up:
emit inputGenerated(QStringLiteral("\x1b[A"));
return;
case Qt::Key_Down:
emit inputGenerated(QStringLiteral("\x1b[B"));
return;
default:
break;
}
const QString text = event->text();
if (!text.isEmpty()) {
emit inputGenerated(text);
return;
}
}
void TerminalView::focusInEvent(QFocusEvent* event)
{
QTextEdit::focusInEvent(event);
moveCursor(QTextCursor::End);
}
void TerminalView::resizeEvent(QResizeEvent* event)
{
QTextEdit::resizeEvent(event);
emitTerminalSize();
}
TerminalView::ThemePalette TerminalView::paletteByName(const QString& themeName)
{
const QString theme = normalizedThemeName(themeName);
if (theme == QStringLiteral("light")) {
return ThemePalette{QStringLiteral("Light"),
QColor(QStringLiteral("#ececec")),
QColor(QStringLiteral("#000000")),
{QColor(QStringLiteral("#000000")),
QColor(QStringLiteral("#aa0000")),
QColor(QStringLiteral("#008000")),
QColor(QStringLiteral("#7a5f00")),
QColor(QStringLiteral("#0033cc")),
QColor(QStringLiteral("#8a00a8")),
QColor(QStringLiteral("#005f87")),
QColor(QStringLiteral("#333333"))},
{QColor(QStringLiteral("#5c5c5c")),
QColor(QStringLiteral("#d30000")),
QColor(QStringLiteral("#00a000")),
QColor(QStringLiteral("#9a7700")),
QColor(QStringLiteral("#0055ff")),
QColor(QStringLiteral("#b300db")),
QColor(QStringLiteral("#007ea7")),
QColor(QStringLiteral("#111111"))}};
}
if (theme == QStringLiteral("solarized dark")) {
return ThemePalette{QStringLiteral("Solarized Dark"),
QColor(QStringLiteral("#002b36")),
QColor(QStringLiteral("#839496")),
{QColor(QStringLiteral("#073642")),
QColor(QStringLiteral("#dc322f")),
QColor(QStringLiteral("#859900")),
QColor(QStringLiteral("#b58900")),
QColor(QStringLiteral("#268bd2")),
QColor(QStringLiteral("#d33682")),
QColor(QStringLiteral("#2aa198")),
QColor(QStringLiteral("#eee8d5"))},
{QColor(QStringLiteral("#586e75")),
QColor(QStringLiteral("#cb4b16")),
QColor(QStringLiteral("#586e75")),
QColor(QStringLiteral("#657b83")),
QColor(QStringLiteral("#839496")),
QColor(QStringLiteral("#6c71c4")),
QColor(QStringLiteral("#93a1a1")),
QColor(QStringLiteral("#fdf6e3"))}};
}
return ThemePalette{QStringLiteral("Dark"),
QColor(QStringLiteral("#1e1e1e")),
QColor(QStringLiteral("#d4d4d4")),
{QColor(QStringLiteral("#000000")),
QColor(QStringLiteral("#cd3131")),
QColor(QStringLiteral("#0dbc79")),
QColor(QStringLiteral("#e5e510")),
QColor(QStringLiteral("#2472c8")),
QColor(QStringLiteral("#bc3fbc")),
QColor(QStringLiteral("#11a8cd")),
QColor(QStringLiteral("#e5e5e5"))},
{QColor(QStringLiteral("#666666")),
QColor(QStringLiteral("#f14c4c")),
QColor(QStringLiteral("#23d18b")),
QColor(QStringLiteral("#f5f543")),
QColor(QStringLiteral("#3b8eea")),
QColor(QStringLiteral("#d670d6")),
QColor(QStringLiteral("#29b8db")),
QColor(QStringLiteral("#ffffff"))}};
}
QColor TerminalView::colorFrom256Index(int index)
{
if (index < 0) {
index = 0;
}
if (index > 255) {
index = 255;
}
if (index < 16) {
static const std::array<QColor, 16> base = {
QColor(QStringLiteral("#000000")), QColor(QStringLiteral("#800000")),
QColor(QStringLiteral("#008000")), QColor(QStringLiteral("#808000")),
QColor(QStringLiteral("#000080")), QColor(QStringLiteral("#800080")),
QColor(QStringLiteral("#008080")), QColor(QStringLiteral("#c0c0c0")),
QColor(QStringLiteral("#808080")), QColor(QStringLiteral("#ff0000")),
QColor(QStringLiteral("#00ff00")), QColor(QStringLiteral("#ffff00")),
QColor(QStringLiteral("#0000ff")), QColor(QStringLiteral("#ff00ff")),
QColor(QStringLiteral("#00ffff")), QColor(QStringLiteral("#ffffff"))};
return base.at(static_cast<size_t>(index));
}
if (index >= 16 && index <= 231) {
const int c = index - 16;
const int r = c / 36;
const int g = (c / 6) % 6;
const int b = c % 6;
const auto channel = [](int v) { return v == 0 ? 0 : 55 + v * 40; };
return QColor(channel(r), channel(g), channel(b));
}
const int gray = 8 + (index - 232) * 10;
return QColor(gray, gray, gray);
}
void TerminalView::applyThemePalette(const ThemePalette& palette)
{
m_palette = palette;
const QString stylesheet = QStringLiteral("QTextEdit { background: %1; color: %2; }")
.arg(m_palette.background.name(), m_palette.foreground.name());
setStyleSheet(stylesheet);
if (!m_hasFgColor) {
m_fgColor = m_palette.foreground;
}
if (!m_hasBgColor) {
m_bgColor = m_palette.background;
}
applyCurrentFormat();
}
void TerminalView::applyCurrentFormat()
{
m_currentFormat = QTextCharFormat();
m_currentFormat.setForeground(m_hasFgColor ? m_fgColor : m_palette.foreground);
if (m_hasBgColor) {
m_currentFormat.setBackground(m_bgColor);
}
QFont font = currentFont();
font.setBold(m_bold);
m_currentFormat.setFont(font);
}
void TerminalView::resetSgrState()
{
m_bold = false;
m_hasFgColor = false;
m_hasBgColor = false;
m_fgColor = m_palette.foreground;
m_bgColor = m_palette.background;
applyCurrentFormat();
}
void TerminalView::handleSgrSequence(const QString& params)
{
QStringList parts = params.split(QChar::fromLatin1(';'), Qt::KeepEmptyParts);
if (parts.isEmpty()) {
parts.push_back(QStringLiteral("0"));
}
for (int i = 0; i < parts.size(); ++i) {
const QString part = parts.at(i).trimmed();
bool ok = false;
const int code = part.isEmpty() ? 0 : part.toInt(&ok);
if (!ok && !part.isEmpty()) {
continue;
}
if (code == 0) {
resetSgrState();
continue;
}
if (code == 1) {
m_bold = true;
continue;
}
if (code == 22) {
m_bold = false;
continue;
}
if (code == 39) {
m_hasFgColor = false;
continue;
}
if (code == 49) {
m_hasBgColor = false;
continue;
}
if (code >= 30 && code <= 37) {
m_fgColor = paletteColor(false, code - 30, false);
m_hasFgColor = true;
continue;
}
if (code >= 90 && code <= 97) {
m_fgColor = paletteColor(false, code - 90, true);
m_hasFgColor = true;
continue;
}
if (code >= 40 && code <= 47) {
m_bgColor = paletteColor(true, code - 40, false);
m_hasBgColor = true;
continue;
}
if (code >= 100 && code <= 107) {
m_bgColor = paletteColor(true, code - 100, true);
m_hasBgColor = true;
continue;
}
if (code == 38 || code == 48) {
const bool background = (code == 48);
if (i + 1 >= parts.size()) {
continue;
}
const int mode = parts.at(i + 1).toInt(&ok);
if (!ok) {
continue;
}
if (mode == 5 && i + 2 < parts.size()) {
const int index = parts.at(i + 2).toInt(&ok);
if (ok) {
const QColor color = colorFrom256Index(index);
if (background) {
m_bgColor = color;
m_hasBgColor = true;
} else {
m_fgColor = color;
m_hasFgColor = true;
}
}
i += 2;
continue;
}
if (mode == 2 && i + 4 < parts.size()) {
const int r = parts.at(i + 2).toInt(&ok);
if (!ok) {
i += 4;
continue;
}
const int g = parts.at(i + 3).toInt(&ok);
if (!ok) {
i += 4;
continue;
}
const int b = parts.at(i + 4).toInt(&ok);
if (!ok) {
i += 4;
continue;
}
const QColor color(r, g, b);
if (background) {
m_bgColor = color;
m_hasBgColor = true;
} else {
m_fgColor = color;
m_hasFgColor = true;
}
i += 4;
continue;
}
}
}
applyCurrentFormat();
}
void TerminalView::appendTextChunk(const QString& text)
{
if (text.isEmpty()) {
return;
}
QTextCursor cursor = textCursor();
cursor.movePosition(QTextCursor::End);
cursor.insertText(text, m_currentFormat);
setTextCursor(cursor);
ensureCursorVisible();
}
QColor TerminalView::paletteColor(bool, int index, bool bright) const
{
const int safeIndex = std::clamp(index, 0, 7);
return bright ? m_palette.bright.at(static_cast<size_t>(safeIndex))
: m_palette.normal.at(static_cast<size_t>(safeIndex));
}
int TerminalView::terminalColumns() const
{
const QFontMetrics metrics(font());
const int cellWidth = std::max(1, metrics.horizontalAdvance(QChar::fromLatin1('M')));
return std::max(1, viewport()->width() / cellWidth);
}
int TerminalView::terminalRows() const
{
const QFontMetrics metrics(font());
const int cellHeight = std::max(1, metrics.lineSpacing());
return std::max(1, viewport()->height() / cellHeight);
}
void TerminalView::emitTerminalSize()
{
emit terminalSizeChanged(terminalColumns(), terminalRows());
}

66
src/terminal_view.h Normal file
View File

@@ -0,0 +1,66 @@
#ifndef ORBITHUB_TERMINAL_VIEW_H
#define ORBITHUB_TERMINAL_VIEW_H
#include <QTextEdit>
#include <array>
class QKeyEvent;
class QFocusEvent;
class QResizeEvent;
class TerminalView : public QTextEdit
{
Q_OBJECT
public:
explicit TerminalView(QWidget* parent = nullptr);
static QStringList themeNames();
void setThemeName(const QString& themeName);
void appendTerminalData(const QString& data);
signals:
void inputGenerated(const QString& input);
void terminalSizeChanged(int columns, int rows);
protected:
void keyPressEvent(QKeyEvent* event) override;
void focusInEvent(QFocusEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
private:
struct ThemePalette {
QString name;
QColor background;
QColor foreground;
std::array<QColor, 8> normal;
std::array<QColor, 8> bright;
};
ThemePalette m_palette;
QString m_pendingEscape;
QString m_rawHistory;
bool m_bold;
bool m_hasFgColor;
bool m_hasBgColor;
QColor m_fgColor;
QColor m_bgColor;
QTextCharFormat m_currentFormat;
static ThemePalette paletteByName(const QString& themeName);
static QColor colorFrom256Index(int index);
void applyThemePalette(const ThemePalette& palette);
void applyCurrentFormat();
void resetSgrState();
void handleSgrSequence(const QString& params);
void appendTextChunk(const QString& text);
QColor paletteColor(bool background, int index, bool bright) const;
void processData(const QString& data, bool storeInHistory);
int terminalColumns() const;
int terminalRows() const;
void emitTerminalSize();
};
#endif

View File

@@ -0,0 +1,39 @@
#include "unsupported_session_backend.h"
UnsupportedSessionBackend::UnsupportedSessionBackend(const Profile& profile, QObject* parent)
: SessionBackend(profile, parent)
{
}
void UnsupportedSessionBackend::connectSession(const SessionConnectOptions&)
{
const QString message = QStringLiteral("Protocol '%1' is not implemented yet.")
.arg(profile().protocol);
emit eventLogged(message);
emit stateChanged(SessionState::Failed, message);
emit connectionError(message, message);
}
void UnsupportedSessionBackend::disconnectSession()
{
emit stateChanged(SessionState::Disconnected,
QStringLiteral("No active connection for this protocol."));
}
void UnsupportedSessionBackend::reconnectSession(const SessionConnectOptions& options)
{
connectSession(options);
}
void UnsupportedSessionBackend::sendInput(const QString&)
{
emit eventLogged(QStringLiteral("Input ignored: protocol backend is not interactive."));
}
void UnsupportedSessionBackend::confirmHostKey(bool)
{
}
void UnsupportedSessionBackend::updateTerminalSize(int, int)
{
}

View File

@@ -0,0 +1,22 @@
#ifndef ORBITHUB_UNSUPPORTED_SESSION_BACKEND_H
#define ORBITHUB_UNSUPPORTED_SESSION_BACKEND_H
#include "session_backend.h"
class UnsupportedSessionBackend : public SessionBackend
{
Q_OBJECT
public:
explicit UnsupportedSessionBackend(const Profile& profile, QObject* parent = nullptr);
public slots:
void connectSession(const SessionConnectOptions& options) override;
void disconnectSession() override;
void reconnectSession(const SessionConnectOptions& options) override;
void sendInput(const QString& input) override;
void confirmHostKey(bool trustHost) override;
void updateTerminalSize(int columns, int rows) override;
};
#endif

61
third_party/KodoTerm/CMakeLists.txt vendored Normal file
View File

@@ -0,0 +1,61 @@
# SPDX-License-Identifier: MIT
cmake_minimum_required(VERSION 3.21)
project(KodoTermVendor LANGUAGES C CXX)
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(LIBVTERM_SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/../libvterm")
file(GLOB VTERM_SOURCES CONFIGURE_DEPENDS
"${LIBVTERM_SOURCE_DIR}/src/*.c"
)
add_library(vterm STATIC ${VTERM_SOURCES})
target_include_directories(vterm PUBLIC
"${LIBVTERM_SOURCE_DIR}/include"
"${LIBVTERM_SOURCE_DIR}/src"
)
set(KODOTERM_SOURCES
src/KodoTerm.cpp
src/KodoTermConfig.cpp
src/PtyProcess.cpp
src/PtyProcess.h
include/KodoTerm/KodoTerm.hpp
include/KodoTerm/KodoTermConfig.hpp
KodoTermThemes.qrc
)
if(UNIX)
list(APPEND KODOTERM_SOURCES
src/PtyProcess_unix.cpp
src/PtyProcess_unix.h
)
elseif(WIN32)
list(APPEND KODOTERM_SOURCES
src/PtyProcess_win.cpp
src/PtyProcess_win.h
)
endif()
add_library(KodoTerm ${KODOTERM_SOURCES})
add_library(KodoTerm::KodoTerm ALIAS KodoTerm)
target_compile_features(KodoTerm PUBLIC cxx_std_20)
target_include_directories(KodoTerm PRIVATE src)
target_include_directories(KodoTerm PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(KodoTerm PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Widgets
vterm
)

470
third_party/KodoTerm/KodoTermThemes.qrc vendored Normal file
View File

@@ -0,0 +1,470 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/">
<file>KodoTermThemes/konsole/BlackOnLightYellow.colorscheme</file>
<file>KodoTermThemes/konsole/BlackOnRandomLight.colorscheme</file>
<file>KodoTermThemes/konsole/BlackOnWhite.colorscheme</file>
<file>KodoTermThemes/konsole/BlueOnBlack.colorscheme</file>
<file>KodoTermThemes/konsole/Breeze.colorscheme</file>
<file>KodoTermThemes/konsole/Campbell.colorscheme</file>
<file>KodoTermThemes/konsole/DarkPastels.colorscheme</file>
<file>KodoTermThemes/konsole/GreenOnBlack.colorscheme</file>
<file>KodoTermThemes/konsole/Linux.colorscheme</file>
<file>KodoTermThemes/konsole/RedOnBlack.colorscheme</file>
<file>KodoTermThemes/konsole/Solarized.colorscheme</file>
<file>KodoTermThemes/konsole/SolarizedLight.colorscheme</file>
<file>KodoTermThemes/konsole/WhiteOnBlack.colorscheme</file>
<file>KodoTermThemes/windowsterminal/0x96f.json</file>
<file>KodoTermThemes/windowsterminal/12-bit Rainbow.json</file>
<file>KodoTermThemes/windowsterminal/3024 Day.json</file>
<file>KodoTermThemes/windowsterminal/3024 Night.json</file>
<file>KodoTermThemes/windowsterminal/Aardvark Blue.json</file>
<file>KodoTermThemes/windowsterminal/Abernathy.json</file>
<file>KodoTermThemes/windowsterminal/Adventure Time.json</file>
<file>KodoTermThemes/windowsterminal/Adventure.json</file>
<file>KodoTermThemes/windowsterminal/Adwaita Dark.json</file>
<file>KodoTermThemes/windowsterminal/Adwaita.json</file>
<file>KodoTermThemes/windowsterminal/Afterglow.json</file>
<file>KodoTermThemes/windowsterminal/Aizen Dark.json</file>
<file>KodoTermThemes/windowsterminal/Aizen Light.json</file>
<file>KodoTermThemes/windowsterminal/Alabaster.json</file>
<file>KodoTermThemes/windowsterminal/Alien Blood.json</file>
<file>KodoTermThemes/windowsterminal/Andromeda.json</file>
<file>KodoTermThemes/windowsterminal/Apple Classic.json</file>
<file>KodoTermThemes/windowsterminal/Apple System Colors Light.json</file>
<file>KodoTermThemes/windowsterminal/Apple System Colors.json</file>
<file>KodoTermThemes/windowsterminal/Arcoiris.json</file>
<file>KodoTermThemes/windowsterminal/Ardoise.json</file>
<file>KodoTermThemes/windowsterminal/Argonaut.json</file>
<file>KodoTermThemes/windowsterminal/Arthur.json</file>
<file>KodoTermThemes/windowsterminal/Atelier Sulphurpool.json</file>
<file>KodoTermThemes/windowsterminal/Atom One Dark.json</file>
<file>KodoTermThemes/windowsterminal/Atom One Light.json</file>
<file>KodoTermThemes/windowsterminal/Atom.json</file>
<file>KodoTermThemes/windowsterminal/Aura.json</file>
<file>KodoTermThemes/windowsterminal/Aurora.json</file>
<file>KodoTermThemes/windowsterminal/Ayu Light.json</file>
<file>KodoTermThemes/windowsterminal/Ayu Mirage.json</file>
<file>KodoTermThemes/windowsterminal/Ayu.json</file>
<file>KodoTermThemes/windowsterminal/Banana Blueberry.json</file>
<file>KodoTermThemes/windowsterminal/Batman.json</file>
<file>KodoTermThemes/windowsterminal/Belafonte Day.json</file>
<file>KodoTermThemes/windowsterminal/Belafonte Night.json</file>
<file>KodoTermThemes/windowsterminal/Birds Of Paradise.json</file>
<file>KodoTermThemes/windowsterminal/Black Metal (Bathory).json</file>
<file>KodoTermThemes/windowsterminal/Black Metal (Burzum).json</file>
<file>KodoTermThemes/windowsterminal/Black Metal (Dark Funeral).json</file>
<file>KodoTermThemes/windowsterminal/Black Metal (Gorgoroth).json</file>
<file>KodoTermThemes/windowsterminal/Black Metal (Immortal).json</file>
<file>KodoTermThemes/windowsterminal/Black Metal (Khold).json</file>
<file>KodoTermThemes/windowsterminal/Black Metal (Marduk).json</file>
<file>KodoTermThemes/windowsterminal/Black Metal (Mayhem).json</file>
<file>KodoTermThemes/windowsterminal/Black Metal (Nile).json</file>
<file>KodoTermThemes/windowsterminal/Black Metal (Venom).json</file>
<file>KodoTermThemes/windowsterminal/Black Metal.json</file>
<file>KodoTermThemes/windowsterminal/Blazer.json</file>
<file>KodoTermThemes/windowsterminal/Blue Berry Pie.json</file>
<file>KodoTermThemes/windowsterminal/Blue Dolphin.json</file>
<file>KodoTermThemes/windowsterminal/Blue Matrix.json</file>
<file>KodoTermThemes/windowsterminal/Bluloco Dark.json</file>
<file>KodoTermThemes/windowsterminal/Bluloco Light.json</file>
<file>KodoTermThemes/windowsterminal/Borland.json</file>
<file>KodoTermThemes/windowsterminal/Box.json</file>
<file>KodoTermThemes/windowsterminal/Breadog.json</file>
<file>KodoTermThemes/windowsterminal/Breeze.json</file>
<file>KodoTermThemes/windowsterminal/Bright Lights.json</file>
<file>KodoTermThemes/windowsterminal/Broadcast.json</file>
<file>KodoTermThemes/windowsterminal/Brogrammer.json</file>
<file>KodoTermThemes/windowsterminal/Builtin Dark.json</file>
<file>KodoTermThemes/windowsterminal/Builtin Light.json</file>
<file>KodoTermThemes/windowsterminal/Builtin Pastel Dark.json</file>
<file>KodoTermThemes/windowsterminal/Builtin Tango Dark.json</file>
<file>KodoTermThemes/windowsterminal/Builtin Tango Light.json</file>
<file>KodoTermThemes/windowsterminal/C64.json</file>
<file>KodoTermThemes/windowsterminal/CGA.json</file>
<file>KodoTermThemes/windowsterminal/CLRS.json</file>
<file>KodoTermThemes/windowsterminal/Calamity.json</file>
<file>KodoTermThemes/windowsterminal/Carbonfox.json</file>
<file>KodoTermThemes/windowsterminal/Catppuccin Frappe.json</file>
<file>KodoTermThemes/windowsterminal/Catppuccin Latte.json</file>
<file>KodoTermThemes/windowsterminal/Catppuccin Macchiato.json</file>
<file>KodoTermThemes/windowsterminal/Catppuccin Mocha.json</file>
<file>KodoTermThemes/windowsterminal/Chalk.json</file>
<file>KodoTermThemes/windowsterminal/Chalkboard.json</file>
<file>KodoTermThemes/windowsterminal/Challenger Deep.json</file>
<file>KodoTermThemes/windowsterminal/Chester.json</file>
<file>KodoTermThemes/windowsterminal/Ciapre.json</file>
<file>KodoTermThemes/windowsterminal/Citruszest.json</file>
<file>KodoTermThemes/windowsterminal/Cobalt Neon.json</file>
<file>KodoTermThemes/windowsterminal/Cobalt Next Dark.json</file>
<file>KodoTermThemes/windowsterminal/Cobalt Next Minimal.json</file>
<file>KodoTermThemes/windowsterminal/Cobalt Next.json</file>
<file>KodoTermThemes/windowsterminal/Cobalt2.json</file>
<file>KodoTermThemes/windowsterminal/Coffee Theme.json</file>
<file>KodoTermThemes/windowsterminal/Crayon Pony Fish.json</file>
<file>KodoTermThemes/windowsterminal/Cursor Dark.json</file>
<file>KodoTermThemes/windowsterminal/Cutie Pro.json</file>
<file>KodoTermThemes/windowsterminal/Cyberdyne.json</file>
<file>KodoTermThemes/windowsterminal/Cyberpunk Scarlet Protocol.json</file>
<file>KodoTermThemes/windowsterminal/Cyberpunk.json</file>
<file>KodoTermThemes/windowsterminal/Dark Modern.json</file>
<file>KodoTermThemes/windowsterminal/Dark Pastel.json</file>
<file>KodoTermThemes/windowsterminal/Dark+.json</file>
<file>KodoTermThemes/windowsterminal/Darkermatrix.json</file>
<file>KodoTermThemes/windowsterminal/Darkmatrix.json</file>
<file>KodoTermThemes/windowsterminal/Darkside.json</file>
<file>KodoTermThemes/windowsterminal/Dawnfox.json</file>
<file>KodoTermThemes/windowsterminal/Dayfox.json</file>
<file>KodoTermThemes/windowsterminal/Deep.json</file>
<file>KodoTermThemes/windowsterminal/Desert.json</file>
<file>KodoTermThemes/windowsterminal/Detuned.json</file>
<file>KodoTermThemes/windowsterminal/Dimidium.json</file>
<file>KodoTermThemes/windowsterminal/Dimmed Monokai.json</file>
<file>KodoTermThemes/windowsterminal/Django Reborn Again.json</file>
<file>KodoTermThemes/windowsterminal/Django Smooth.json</file>
<file>KodoTermThemes/windowsterminal/Django.json</file>
<file>KodoTermThemes/windowsterminal/Doom One.json</file>
<file>KodoTermThemes/windowsterminal/Doom Peacock.json</file>
<file>KodoTermThemes/windowsterminal/Dot Gov.json</file>
<file>KodoTermThemes/windowsterminal/Dracula+.json</file>
<file>KodoTermThemes/windowsterminal/Dracula.json</file>
<file>KodoTermThemes/windowsterminal/Duckbones.json</file>
<file>KodoTermThemes/windowsterminal/Duotone Dark.json</file>
<file>KodoTermThemes/windowsterminal/Duskfox.json</file>
<file>KodoTermThemes/windowsterminal/ENCOM.json</file>
<file>KodoTermThemes/windowsterminal/Earthsong.json</file>
<file>KodoTermThemes/windowsterminal/Electron Highlighter.json</file>
<file>KodoTermThemes/windowsterminal/Elegant.json</file>
<file>KodoTermThemes/windowsterminal/Elemental.json</file>
<file>KodoTermThemes/windowsterminal/Elementary.json</file>
<file>KodoTermThemes/windowsterminal/Embark.json</file>
<file>KodoTermThemes/windowsterminal/Embers Dark.json</file>
<file>KodoTermThemes/windowsterminal/Espresso Libre.json</file>
<file>KodoTermThemes/windowsterminal/Espresso.json</file>
<file>KodoTermThemes/windowsterminal/Everblush.json</file>
<file>KodoTermThemes/windowsterminal/Everforest Dark Hard.json</file>
<file>KodoTermThemes/windowsterminal/Everforest Light Med.json</file>
<file>KodoTermThemes/windowsterminal/Fahrenheit.json</file>
<file>KodoTermThemes/windowsterminal/Fairyfloss.json</file>
<file>KodoTermThemes/windowsterminal/Farmhouse Dark.json</file>
<file>KodoTermThemes/windowsterminal/Farmhouse Light.json</file>
<file>KodoTermThemes/windowsterminal/Fideloper.json</file>
<file>KodoTermThemes/windowsterminal/Firefly Traditional.json</file>
<file>KodoTermThemes/windowsterminal/Firefox Dev.json</file>
<file>KodoTermThemes/windowsterminal/Firewatch.json</file>
<file>KodoTermThemes/windowsterminal/Fish Tank.json</file>
<file>KodoTermThemes/windowsterminal/Flat.json</file>
<file>KodoTermThemes/windowsterminal/Flatland.json</file>
<file>KodoTermThemes/windowsterminal/Flexoki Dark.json</file>
<file>KodoTermThemes/windowsterminal/Flexoki Light.json</file>
<file>KodoTermThemes/windowsterminal/Floraverse.json</file>
<file>KodoTermThemes/windowsterminal/Forest Blue.json</file>
<file>KodoTermThemes/windowsterminal/Framer.json</file>
<file>KodoTermThemes/windowsterminal/Front End Delight.json</file>
<file>KodoTermThemes/windowsterminal/Fun Forrest.json</file>
<file>KodoTermThemes/windowsterminal/Galaxy.json</file>
<file>KodoTermThemes/windowsterminal/Galizur.json</file>
<file>KodoTermThemes/windowsterminal/Ghostty Default Style Dark.json</file>
<file>KodoTermThemes/windowsterminal/GitHub Dark Colorblind.json</file>
<file>KodoTermThemes/windowsterminal/GitHub Dark Default.json</file>
<file>KodoTermThemes/windowsterminal/GitHub Dark Dimmed.json</file>
<file>KodoTermThemes/windowsterminal/GitHub Dark High Contrast.json</file>
<file>KodoTermThemes/windowsterminal/GitHub Dark.json</file>
<file>KodoTermThemes/windowsterminal/GitHub Light Colorblind.json</file>
<file>KodoTermThemes/windowsterminal/GitHub Light Default.json</file>
<file>KodoTermThemes/windowsterminal/GitHub Light High Contrast.json</file>
<file>KodoTermThemes/windowsterminal/GitHub.json</file>
<file>KodoTermThemes/windowsterminal/GitLab Dark Grey.json</file>
<file>KodoTermThemes/windowsterminal/GitLab Dark.json</file>
<file>KodoTermThemes/windowsterminal/GitLab Light.json</file>
<file>KodoTermThemes/windowsterminal/Glacier.json</file>
<file>KodoTermThemes/windowsterminal/Grape.json</file>
<file>KodoTermThemes/windowsterminal/Grass.json</file>
<file>KodoTermThemes/windowsterminal/Grey Green.json</file>
<file>KodoTermThemes/windowsterminal/Gruber Darker.json</file>
<file>KodoTermThemes/windowsterminal/Gruvbox Dark Hard.json</file>
<file>KodoTermThemes/windowsterminal/Gruvbox Dark.json</file>
<file>KodoTermThemes/windowsterminal/Gruvbox Light Hard.json</file>
<file>KodoTermThemes/windowsterminal/Gruvbox Light.json</file>
<file>KodoTermThemes/windowsterminal/Gruvbox Material Dark.json</file>
<file>KodoTermThemes/windowsterminal/Gruvbox Material Light.json</file>
<file>KodoTermThemes/windowsterminal/Gruvbox Material.json</file>
<file>KodoTermThemes/windowsterminal/Guezwhoz.json</file>
<file>KodoTermThemes/windowsterminal/HaX0R Blue.json</file>
<file>KodoTermThemes/windowsterminal/HaX0R Gr33N.json</file>
<file>KodoTermThemes/windowsterminal/HaX0R R3D.json</file>
<file>KodoTermThemes/windowsterminal/Hacktober.json</file>
<file>KodoTermThemes/windowsterminal/Hardcore.json</file>
<file>KodoTermThemes/windowsterminal/Harper.json</file>
<file>KodoTermThemes/windowsterminal/Havn Daggry.json</file>
<file>KodoTermThemes/windowsterminal/Havn Skumring.json</file>
<file>KodoTermThemes/windowsterminal/Heeler.json</file>
<file>KodoTermThemes/windowsterminal/Highway.json</file>
<file>KodoTermThemes/windowsterminal/Hipster Green.json</file>
<file>KodoTermThemes/windowsterminal/Hivacruz.json</file>
<file>KodoTermThemes/windowsterminal/Homebrew.json</file>
<file>KodoTermThemes/windowsterminal/Hopscotch.256.json</file>
<file>KodoTermThemes/windowsterminal/Hopscotch.json</file>
<file>KodoTermThemes/windowsterminal/Horizon Bright.json</file>
<file>KodoTermThemes/windowsterminal/Horizon.json</file>
<file>KodoTermThemes/windowsterminal/Hot Dog Stand (Mustard).json</file>
<file>KodoTermThemes/windowsterminal/Hot Dog Stand.json</file>
<file>KodoTermThemes/windowsterminal/Hurtado.json</file>
<file>KodoTermThemes/windowsterminal/Hybrid.json</file>
<file>KodoTermThemes/windowsterminal/IBM 5153 CGA (Black).json</file>
<file>KodoTermThemes/windowsterminal/IBM 5153 CGA.json</file>
<file>KodoTermThemes/windowsterminal/IC Green PPL.json</file>
<file>KodoTermThemes/windowsterminal/IC Orange PPL.json</file>
<file>KodoTermThemes/windowsterminal/IR Black.json</file>
<file>KodoTermThemes/windowsterminal/IRIX Console.json</file>
<file>KodoTermThemes/windowsterminal/IRIX Terminal.json</file>
<file>KodoTermThemes/windowsterminal/Iceberg Dark.json</file>
<file>KodoTermThemes/windowsterminal/Iceberg Light.json</file>
<file>KodoTermThemes/windowsterminal/Idea.json</file>
<file>KodoTermThemes/windowsterminal/Idle Toes.json</file>
<file>KodoTermThemes/windowsterminal/Jackie Brown.json</file>
<file>KodoTermThemes/windowsterminal/Japanesque.json</file>
<file>KodoTermThemes/windowsterminal/Jellybeans.json</file>
<file>KodoTermThemes/windowsterminal/JetBrains Darcula.json</file>
<file>KodoTermThemes/windowsterminal/Jubi.json</file>
<file>KodoTermThemes/windowsterminal/Kanagawa Dragon.json</file>
<file>KodoTermThemes/windowsterminal/Kanagawa Wave.json</file>
<file>KodoTermThemes/windowsterminal/Kanagawabones.json</file>
<file>KodoTermThemes/windowsterminal/Kibble.json</file>
<file>KodoTermThemes/windowsterminal/Kitty Default.json</file>
<file>KodoTermThemes/windowsterminal/Kitty Low Contrast.json</file>
<file>KodoTermThemes/windowsterminal/Kolorit.json</file>
<file>KodoTermThemes/windowsterminal/Konsolas.json</file>
<file>KodoTermThemes/windowsterminal/Kurokula.json</file>
<file>KodoTermThemes/windowsterminal/Lab Fox.json</file>
<file>KodoTermThemes/windowsterminal/Laser.json</file>
<file>KodoTermThemes/windowsterminal/Later This Evening.json</file>
<file>KodoTermThemes/windowsterminal/Lavandula.json</file>
<file>KodoTermThemes/windowsterminal/Light Owl.json</file>
<file>KodoTermThemes/windowsterminal/Liquid Carbon Transparent.json</file>
<file>KodoTermThemes/windowsterminal/Liquid Carbon.json</file>
<file>KodoTermThemes/windowsterminal/Lovelace.json</file>
<file>KodoTermThemes/windowsterminal/Man Page.json</file>
<file>KodoTermThemes/windowsterminal/Mariana.json</file>
<file>KodoTermThemes/windowsterminal/Material Dark.json</file>
<file>KodoTermThemes/windowsterminal/Material Darker.json</file>
<file>KodoTermThemes/windowsterminal/Material Design Colors.json</file>
<file>KodoTermThemes/windowsterminal/Material Ocean.json</file>
<file>KodoTermThemes/windowsterminal/Material.json</file>
<file>KodoTermThemes/windowsterminal/Mathias.json</file>
<file>KodoTermThemes/windowsterminal/Matrix.json</file>
<file>KodoTermThemes/windowsterminal/Matte Black.json</file>
<file>KodoTermThemes/windowsterminal/Medallion.json</file>
<file>KodoTermThemes/windowsterminal/Melange Dark.json</file>
<file>KodoTermThemes/windowsterminal/Melange Light.json</file>
<file>KodoTermThemes/windowsterminal/Mellifluous.json</file>
<file>KodoTermThemes/windowsterminal/Mellow.json</file>
<file>KodoTermThemes/windowsterminal/Miasma.json</file>
<file>KodoTermThemes/windowsterminal/Midnight In Mojave.json</file>
<file>KodoTermThemes/windowsterminal/Mirage.json</file>
<file>KodoTermThemes/windowsterminal/Misterioso.json</file>
<file>KodoTermThemes/windowsterminal/Molokai.json</file>
<file>KodoTermThemes/windowsterminal/Mona Lisa.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Classic.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Pro Light Sun.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Pro Light.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Pro Machine.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Pro Octagon.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Pro Ristretto.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Pro Spectrum.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Pro.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Remastered.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Soda.json</file>
<file>KodoTermThemes/windowsterminal/Monokai Vivid.json</file>
<file>KodoTermThemes/windowsterminal/Moonfly.json</file>
<file>KodoTermThemes/windowsterminal/N0Tch2K.json</file>
<file>KodoTermThemes/windowsterminal/Neobones Dark.json</file>
<file>KodoTermThemes/windowsterminal/Neobones Light.json</file>
<file>KodoTermThemes/windowsterminal/Neon.json</file>
<file>KodoTermThemes/windowsterminal/Neopolitan.json</file>
<file>KodoTermThemes/windowsterminal/Neutron.json</file>
<file>KodoTermThemes/windowsterminal/Night Lion V1.json</file>
<file>KodoTermThemes/windowsterminal/Night Lion V2.json</file>
<file>KodoTermThemes/windowsterminal/Night Owl.json</file>
<file>KodoTermThemes/windowsterminal/Night Owlish Light.json</file>
<file>KodoTermThemes/windowsterminal/Nightfox.json</file>
<file>KodoTermThemes/windowsterminal/Niji.json</file>
<file>KodoTermThemes/windowsterminal/No Clown Fiesta Light.json</file>
<file>KodoTermThemes/windowsterminal/No Clown Fiesta.json</file>
<file>KodoTermThemes/windowsterminal/Nocturnal Winter.json</file>
<file>KodoTermThemes/windowsterminal/Nord Light.json</file>
<file>KodoTermThemes/windowsterminal/Nord Wave.json</file>
<file>KodoTermThemes/windowsterminal/Nord.json</file>
<file>KodoTermThemes/windowsterminal/Nordfox.json</file>
<file>KodoTermThemes/windowsterminal/Novel.json</file>
<file>KodoTermThemes/windowsterminal/Nvim Dark.json</file>
<file>KodoTermThemes/windowsterminal/Nvim Light.json</file>
<file>KodoTermThemes/windowsterminal/Obsidian.json</file>
<file>KodoTermThemes/windowsterminal/Ocean.json</file>
<file>KodoTermThemes/windowsterminal/Oceanic Material.json</file>
<file>KodoTermThemes/windowsterminal/Oceanic Next.json</file>
<file>KodoTermThemes/windowsterminal/Ollie.json</file>
<file>KodoTermThemes/windowsterminal/One Dark Two.json</file>
<file>KodoTermThemes/windowsterminal/One Double Dark.json</file>
<file>KodoTermThemes/windowsterminal/One Double Light.json</file>
<file>KodoTermThemes/windowsterminal/One Half Dark.json</file>
<file>KodoTermThemes/windowsterminal/One Half Light.json</file>
<file>KodoTermThemes/windowsterminal/Onenord Light.json</file>
<file>KodoTermThemes/windowsterminal/Onenord.json</file>
<file>KodoTermThemes/windowsterminal/Operator Mono Dark.json</file>
<file>KodoTermThemes/windowsterminal/Overnight Slumber.json</file>
<file>KodoTermThemes/windowsterminal/Oxocarbon.json</file>
<file>KodoTermThemes/windowsterminal/Pale Night Hc.json</file>
<file>KodoTermThemes/windowsterminal/Pandora.json</file>
<file>KodoTermThemes/windowsterminal/Paraiso Dark.json</file>
<file>KodoTermThemes/windowsterminal/Paul Millr.json</file>
<file>KodoTermThemes/windowsterminal/Pencil Dark.json</file>
<file>KodoTermThemes/windowsterminal/Pencil Light.json</file>
<file>KodoTermThemes/windowsterminal/Peppermint.json</file>
<file>KodoTermThemes/windowsterminal/Phala Green Dark.json</file>
<file>KodoTermThemes/windowsterminal/Piatto Light.json</file>
<file>KodoTermThemes/windowsterminal/Pnevma.json</file>
<file>KodoTermThemes/windowsterminal/Poimandres Darker.json</file>
<file>KodoTermThemes/windowsterminal/Poimandres Storm.json</file>
<file>KodoTermThemes/windowsterminal/Poimandres White.json</file>
<file>KodoTermThemes/windowsterminal/Poimandres.json</file>
<file>KodoTermThemes/windowsterminal/Popping And Locking.json</file>
<file>KodoTermThemes/windowsterminal/Powershell.json</file>
<file>KodoTermThemes/windowsterminal/Primary.json</file>
<file>KodoTermThemes/windowsterminal/Pro Light.json</file>
<file>KodoTermThemes/windowsterminal/Pro.json</file>
<file>KodoTermThemes/windowsterminal/Purple Rain.json</file>
<file>KodoTermThemes/windowsterminal/Purplepeter.json</file>
<file>KodoTermThemes/windowsterminal/Rapture.json</file>
<file>KodoTermThemes/windowsterminal/Raycast Dark.json</file>
<file>KodoTermThemes/windowsterminal/Raycast Light.json</file>
<file>KodoTermThemes/windowsterminal/Rebecca.json</file>
<file>KodoTermThemes/windowsterminal/Red Alert.json</file>
<file>KodoTermThemes/windowsterminal/Red Planet.json</file>
<file>KodoTermThemes/windowsterminal/Red Sands.json</file>
<file>KodoTermThemes/windowsterminal/Relaxed.json</file>
<file>KodoTermThemes/windowsterminal/Retro Legends.json</file>
<file>KodoTermThemes/windowsterminal/Retro.json</file>
<file>KodoTermThemes/windowsterminal/Rippedcasts.json</file>
<file>KodoTermThemes/windowsterminal/Rose Pine Dawn.json</file>
<file>KodoTermThemes/windowsterminal/Rose Pine Moon.json</file>
<file>KodoTermThemes/windowsterminal/Rose Pine.json</file>
<file>KodoTermThemes/windowsterminal/Rouge 2.json</file>
<file>KodoTermThemes/windowsterminal/Royal.json</file>
<file>KodoTermThemes/windowsterminal/Ryuuko.json</file>
<file>KodoTermThemes/windowsterminal/Sakura.json</file>
<file>KodoTermThemes/windowsterminal/Scarlet Protocol.json</file>
<file>KodoTermThemes/windowsterminal/Sea Shells.json</file>
<file>KodoTermThemes/windowsterminal/Seafoam Pastel.json</file>
<file>KodoTermThemes/windowsterminal/Selenized Black.json</file>
<file>KodoTermThemes/windowsterminal/Selenized Dark.json</file>
<file>KodoTermThemes/windowsterminal/Selenized Light.json</file>
<file>KodoTermThemes/windowsterminal/Seoulbones Dark.json</file>
<file>KodoTermThemes/windowsterminal/Seoulbones Light.json</file>
<file>KodoTermThemes/windowsterminal/Seti.json</file>
<file>KodoTermThemes/windowsterminal/Shades Of Purple.json</file>
<file>KodoTermThemes/windowsterminal/Shaman.json</file>
<file>KodoTermThemes/windowsterminal/Slate.json</file>
<file>KodoTermThemes/windowsterminal/Sleepy Hollow.json</file>
<file>KodoTermThemes/windowsterminal/Smyck.json</file>
<file>KodoTermThemes/windowsterminal/Snazzy Soft.json</file>
<file>KodoTermThemes/windowsterminal/Snazzy.json</file>
<file>KodoTermThemes/windowsterminal/Soft Server.json</file>
<file>KodoTermThemes/windowsterminal/Solarized Darcula.json</file>
<file>KodoTermThemes/windowsterminal/Solarized Dark Higher Contrast.json</file>
<file>KodoTermThemes/windowsterminal/Solarized Dark Patched.json</file>
<file>KodoTermThemes/windowsterminal/Solarized Osaka Night.json</file>
<file>KodoTermThemes/windowsterminal/Sonokai.json</file>
<file>KodoTermThemes/windowsterminal/Spacedust.json</file>
<file>KodoTermThemes/windowsterminal/Spacegray Bright.json</file>
<file>KodoTermThemes/windowsterminal/Spacegray Eighties Dull.json</file>
<file>KodoTermThemes/windowsterminal/Spacegray Eighties.json</file>
<file>KodoTermThemes/windowsterminal/Spacegray.json</file>
<file>KodoTermThemes/windowsterminal/Spiderman.json</file>
<file>KodoTermThemes/windowsterminal/Spring.json</file>
<file>KodoTermThemes/windowsterminal/Square.json</file>
<file>KodoTermThemes/windowsterminal/Squirrelsong Dark.json</file>
<file>KodoTermThemes/windowsterminal/Srcery.json</file>
<file>KodoTermThemes/windowsterminal/Starlight.json</file>
<file>KodoTermThemes/windowsterminal/Sublette.json</file>
<file>KodoTermThemes/windowsterminal/Subliminal.json</file>
<file>KodoTermThemes/windowsterminal/Sugarplum.json</file>
<file>KodoTermThemes/windowsterminal/Sundried.json</file>
<file>KodoTermThemes/windowsterminal/Symfonic.json</file>
<file>KodoTermThemes/windowsterminal/Synthwave Alpha.json</file>
<file>KodoTermThemes/windowsterminal/Synthwave Everything.json</file>
<file>KodoTermThemes/windowsterminal/Synthwave.json</file>
<file>KodoTermThemes/windowsterminal/Tango Adapted.json</file>
<file>KodoTermThemes/windowsterminal/Tango Half Adapted.json</file>
<file>KodoTermThemes/windowsterminal/Tearout.json</file>
<file>KodoTermThemes/windowsterminal/Teerb.json</file>
<file>KodoTermThemes/windowsterminal/Terafox.json</file>
<file>KodoTermThemes/windowsterminal/Terminal Basic Dark.json</file>
<file>KodoTermThemes/windowsterminal/Terminal Basic.json</file>
<file>KodoTermThemes/windowsterminal/Thayer Bright.json</file>
<file>KodoTermThemes/windowsterminal/The Hulk.json</file>
<file>KodoTermThemes/windowsterminal/Tinacious Design Dark.json</file>
<file>KodoTermThemes/windowsterminal/Tinacious Design Light.json</file>
<file>KodoTermThemes/windowsterminal/TokyoNight Day.json</file>
<file>KodoTermThemes/windowsterminal/TokyoNight Moon.json</file>
<file>KodoTermThemes/windowsterminal/TokyoNight Night.json</file>
<file>KodoTermThemes/windowsterminal/TokyoNight Storm.json</file>
<file>KodoTermThemes/windowsterminal/TokyoNight.json</file>
<file>KodoTermThemes/windowsterminal/Tomorrow Night Blue.json</file>
<file>KodoTermThemes/windowsterminal/Tomorrow Night Bright.json</file>
<file>KodoTermThemes/windowsterminal/Tomorrow Night Burns.json</file>
<file>KodoTermThemes/windowsterminal/Tomorrow Night Eighties.json</file>
<file>KodoTermThemes/windowsterminal/Tomorrow Night.json</file>
<file>KodoTermThemes/windowsterminal/Tomorrow.json</file>
<file>KodoTermThemes/windowsterminal/Toy Chest.json</file>
<file>KodoTermThemes/windowsterminal/Treehouse.json</file>
<file>KodoTermThemes/windowsterminal/Twilight.json</file>
<file>KodoTermThemes/windowsterminal/Ubuntu.json</file>
<file>KodoTermThemes/windowsterminal/Ultra Dark.json</file>
<file>KodoTermThemes/windowsterminal/Ultra Violent.json</file>
<file>KodoTermThemes/windowsterminal/Under The Sea.json</file>
<file>KodoTermThemes/windowsterminal/Unikitty.json</file>
<file>KodoTermThemes/windowsterminal/Urple.json</file>
<file>KodoTermThemes/windowsterminal/Vague.json</file>
<file>KodoTermThemes/windowsterminal/Vaughn.json</file>
<file>KodoTermThemes/windowsterminal/Vercel.json</file>
<file>KodoTermThemes/windowsterminal/Vesper.json</file>
<file>KodoTermThemes/windowsterminal/Vibrant Ink.json</file>
<file>KodoTermThemes/windowsterminal/Vimbones.json</file>
<file>KodoTermThemes/windowsterminal/Violet Dark.json</file>
<file>KodoTermThemes/windowsterminal/Violet Light.json</file>
<file>KodoTermThemes/windowsterminal/Violite.json</file>
<file>KodoTermThemes/windowsterminal/Warm Neon.json</file>
<file>KodoTermThemes/windowsterminal/Wez.json</file>
<file>KodoTermThemes/windowsterminal/Whimsy.json</file>
<file>KodoTermThemes/windowsterminal/Wild Cherry.json</file>
<file>KodoTermThemes/windowsterminal/Wilmersdorf.json</file>
<file>KodoTermThemes/windowsterminal/Wombat.json</file>
<file>KodoTermThemes/windowsterminal/Wryan.json</file>
<file>KodoTermThemes/windowsterminal/Xcode Dark hc.json</file>
<file>KodoTermThemes/windowsterminal/Xcode Dark.json</file>
<file>KodoTermThemes/windowsterminal/Xcode Light hc.json</file>
<file>KodoTermThemes/windowsterminal/Xcode Light.json</file>
<file>KodoTermThemes/windowsterminal/Xcode WWDC.json</file>
<file>KodoTermThemes/windowsterminal/Zenbones Dark.json</file>
<file>KodoTermThemes/windowsterminal/Zenbones Light.json</file>
<file>KodoTermThemes/windowsterminal/Zenbones.json</file>
<file>KodoTermThemes/windowsterminal/Zenburn.json</file>
<file>KodoTermThemes/windowsterminal/Zenburned.json</file>
<file>KodoTermThemes/windowsterminal/Zenwritten Dark.json</file>
<file>KodoTermThemes/windowsterminal/Zenwritten Light.json</file>
<file>KodoTermThemes/windowsterminal/branch.json</file>
<file>KodoTermThemes/windowsterminal/iTerm2 Dark Background.json</file>
<file>KodoTermThemes/windowsterminal/iTerm2 Default.json</file>
<file>KodoTermThemes/windowsterminal/iTerm2 Light Background.json</file>
<file>KodoTermThemes/windowsterminal/iTerm2 Pastel Dark Background.json</file>
<file>KodoTermThemes/windowsterminal/iTerm2 Smoooooth.json</file>
<file>KodoTermThemes/windowsterminal/iTerm2 Solarized Dark.json</file>
<file>KodoTermThemes/windowsterminal/iTerm2 Solarized Light.json</file>
<file>KodoTermThemes/windowsterminal/iTerm2 Tango Dark.json</file>
<file>KodoTermThemes/windowsterminal/iTerm2 Tango Light.json</file>
<file>KodoTermThemes/windowsterminal/novmbr.json</file>
<file>KodoTermThemes/windowsterminal/owl.json</file>
<file>KodoTermThemes/windowsterminal/traffic.json</file>
<file>KodoTermThemes/windowsterminal/urban.json</file>
</qresource>
</RCC>

View File

@@ -0,0 +1,94 @@
[Background]
Color=255,255,221
[BackgroundIntense]
Color=255,255,221
[BackgroundFaint]
Color=255,255,221
[Color0]
Color=0,0,0
[Color0Intense]
Color=104,104,104
[Color0Faint]
Color=192,192,192
[Color1]
Color=178,24,24
[Color1Intense]
Color=255,84,84
[Color1Faint]
Color=224,142,142
[Color2]
Color=24,178,24
[Color2Intense]
Color=84,255,84
[Color2Faint]
Color=142,224,142
[Color3]
Color=178,104,24
[Color3Intense]
Color=255,255,84
[Color3Faint]
Color=224,224,142
[Color4]
Color=24,24,178
[Color4Intense]
Color=84,84,255
[Color4Faint]
Color=142,142,224
[Color5]
Color=178,24,178
[Color5Intense]
Color=255,84,255
[Color5Faint]
Color=224,142,224
[Color6]
Color=24,178,178
[Color6Intense]
Color=84,255,255
[Color6Faint]
Color=142,224,224
[Color7]
Color=178,178,178
[Color7Intense]
Color=255,255,255
[Color7Faint]
Color=142,142,142
[Foreground]
Color=0,0,0
[ForegroundIntense]
Bold=true
Color=0,0,0
[ForegroundFaint]
Color=0,0,0
[General]
Description=Black on Light Yellow
Opacity=1

View File

@@ -0,0 +1,104 @@
[Background]
Color=247,247,214
RandomHueRange=360
RandomSaturationRange=25
RandomLightnessRange=10
[BackgroundIntense]
Color=255,255,221
RandomHueRange=360
RandomSaturationRange=25
RandomLightnessRange=10
[BackgroundFaint]
Color=247,247,214
RandomHueRange=360
RandomSaturationRange=25
RandomLightnessRange=10
[Color0]
Color=0,0,0
[Color0Intense]
Color=104,104,104
[Color0Faint]
Color=192,192,192
[Color1]
Color=178,24,24
[Color1Intense]
Color=255,84,84
[Color1Faint]
Color=224,142,142
[Color2]
Color=24,178,24
[Color2Intense]
Color=84,255,84
[Color2Faint]
Color=142,224,142
[Color3]
Color=178,104,24
[Color3Intense]
Color=255,255,84
[Color3Faint]
Color=224,224,142
[Color4]
Color=24,24,178
[Color4Intense]
Color=84,84,255
[Color4Faint]
Color=142,142,224
[Color5]
Color=178,24,178
[Color5Intense]
Color=255,84,255
[Color5Faint]
Color=224,142,224
[Color6]
Color=24,178,178
[Color6Intense]
Color=84,255,255
[Color6Faint]
Color=142,224,224
[Color7]
Color=178,178,178
[Color7Intense]
Color=255,255,255
[Color7Faint]
Color=142,142,142
[Foreground]
Color=0,0,0
[ForegroundIntense]
Bold=true
Color=0,0,0
[ForegroundFaint]
Color=0,0,0
[General]
Description=Black on Random Light
ColorRandomization=true
Opacity=1

View File

@@ -0,0 +1,94 @@
[Background]
Color=255,255,255
[BackgroundIntense]
Color=255,255,255
[BackgroundFaint]
Color=255,255,255
[Color0]
Color=0,0,0
[Color0Intense]
Color=104,104,104
[Color0Faint]
Color=192,192,192
[Color1]
Color=178,24,24
[Color1Intense]
Color=255,84,84
[Color1Faint]
Color=224,142,142
[Color2]
Color=24,178,24
[Color2Intense]
Color=84,255,84
[Color2Faint]
Color=142,224,142
[Color3]
Color=178,104,24
[Color3Intense]
Color=255,255,84
[Color3Faint]
Color=224,224,142
[Color4]
Color=24,24,178
[Color4Intense]
Color=84,84,255
[Color4Faint]
Color=142,142,224
[Color5]
Color=178,24,178
[Color5Intense]
Color=255,84,255
[Color5Faint]
Color=224,142,224
[Color6]
Color=24,178,178
[Color6Intense]
Color=84,255,255
[Color6Faint]
Color=142,224,224
[Color7]
Color=178,178,178
[Color7Intense]
Color=255,255,255
[Color7Faint]
Color=142,142,142
[Foreground]
Color=0,0,0
[ForegroundIntense]
Bold=true
Color=0,0,0
[ForegroundFaint]
Color=0,0,0
[General]
Description=Black on White
Opacity=1

View File

@@ -0,0 +1,94 @@
[Background]
Color=0,0,0
[BackgroundIntense]
Color=0,0,0
[BackgroundFaint]
Color=0,0,0
[Color0]
Color=0,0,0
[Color0Intense]
Color=104,104,104
[Color0Faint]
Color=192,192,192
[Color1]
Color=250,0,0
[Color1Intense]
Color=75,93,255
[Color1Faint]
Color=250,0,0
[Color2]
Color=24,178,24
[Color2Intense]
Color=84,255,84
[Color2Faint]
Color=142,224,142
[Color3]
Color=178,104,24
[Color3Intense]
Color=255,255,84
[Color3Faint]
Color=224,224,142
[Color4]
Color=125,152,35
[Color4Intense]
Color=84,84,255
[Color4Faint]
Color=125,152,35
[Color5]
Color=225,30,225
[Color5Intense]
Color=255,84,255
[Color5Faint]
Color=175,29,175
[Color6]
Color=0,134,223
[Color6Intense]
Color=0,68,255
[Color6Faint]
Color=0,98,173
[Color7]
Color=255,255,255
[Color7Intense]
Color=50,50,50
[Color7Faint]
Color=200,200,200
[Foreground]
Color=0,119,255
[ForegroundIntense]
Bold=true
Color=23,74,240
[ForegroundFaint]
Color=0,90,195
[General]
Description=Blue on Black
Opacity=1

View File

@@ -0,0 +1,94 @@
[Background]
Color=35,38,39
[BackgroundFaint]
Color=49,54,59
[BackgroundIntense]
Color=0,0,0
[Color0]
Color=35,38,39
[Color0Faint]
Color=49,54,59
[Color0Intense]
Color=127,140,141
[Color1]
Color=237,21,21
[Color1Faint]
Color=120,50,40
[Color1Intense]
Color=192,57,43
[Color2]
Color=17,209,22
[Color2Faint]
Color=23,162,98
[Color2Intense]
Color=28,220,154
[Color3]
Color=246,116,0
[Color3Faint]
Color=182,86,25
[Color3Intense]
Color=253,188,75
[Color4]
Color=29,153,243
[Color4Faint]
Color=27,102,143
[Color4Intense]
Color=61,174,233
[Color5]
Color=155,89,182
[Color5Faint]
Color=97,74,115
[Color5Intense]
Color=142,68,173
[Color6]
Color=26,188,156
[Color6Faint]
Color=24,108,96
[Color6Intense]
Color=22,160,133
[Color7]
Color=252,252,252
[Color7Faint]
Color=99,104,109
[Color7Intense]
Color=255,255,255
[Foreground]
Color=252,252,252
[ForegroundFaint]
Color=239,240,241
[ForegroundIntense]
Color=61,174,233
[General]
Description=Breeze
Opacity=1
Wallpaper=

View File

@@ -0,0 +1,100 @@
[Background]
Color=12,12,12
[BackgroundFaint]
Color=44,44,44
[BackgroundIntense]
Color=0,0,0
[Color0]
Color=12,12,12
[Color0Faint]
Color=44,44,44
[Color0Intense]
Color=124,124,124
[Color1]
Color=197,15,31
[Color1Faint]
Color=150,11,25
[Color1Intense]
Color=231,72,86
[Color2]
Color=19,161,14
[Color2Faint]
Color=16,120,10
[Color2Intense]
Color=22,198,12
[Color3]
Color=193,156,0
[Color3Faint]
Color=150,120,0
[Color3Intense]
Color=249,241,165
[Color4]
Color=0,55,218
[Color4Faint]
Color=0,45,170
[Color4Intense]
Color=59,120,255
[Color5]
Color=136,23,152
[Color5Faint]
Color=106,18,120
[Color5Intense]
Color=180,0,158
[Color6]
Color=58,150,221
[Color6Faint]
Color=47,122,180
[Color6Intense]
Color=97,214,214
[Color7]
Color=204,204,204
[Color7Faint]
Color=170,170,170
[Color7Intense]
Color=242,242,242
[Foreground]
Color=204,204,204
[ForegroundFaint]
Color=170,170,170
[ForegroundIntense]
Color=255,255,255
[General]
Anchor=0.5,0.5
Blur=false
ColorRandomization=false
Description=Campbell
FillStyle=Tile
Opacity=1
Wallpaper=
WallpaperFlipType=NoFlip
WallpaperOpacity=1

View File

@@ -0,0 +1,103 @@
[Background]
Color=44,44,44
[BackgroundIntense]
Bold=true
Color=44,44,44
[BackgroundFaint]
Color=44,44,44
[Color0]
Color=63,63,63
[Color0Intense]
Bold=true
Color=112,144,128
[Color0Faint]
Color=52,52,52
[Color1]
Color=112,80,80
[Color1Intense]
Bold=true
Color=220,163,163
[Color1Faint]
Color=102,72,72
[Color2]
Color=96,180,138
[Color2Intense]
Bold=true
Color=114,213,163
[Color2Faint]
Color=87,163,124
[Color3]
Color=223,175,143
[Color3Intense]
Bold=true
Color=240,223,175
[Color3Faint]
Color=170,133,111
[Color4]
Color=154,184,215
[Color4Intense]
Bold=true
Color=148,191,243
[Color4Faint]
Color=117,141,161
[Color5]
Color=220,140,195
[Color5Intense]
Bold=true
Color=236,147,211
[Color5Faint]
Color=154,98,137
[Color6]
Color=140,208,211
[Color6Intense]
Bold=true
Color=147,224,227
[Color6Faint]
Color=107,159,161
[Color7]
Color=220,220,204
[Color7Intense]
Bold=true
Color=255,255,255
[Color7Faint]
Color=149,149,139
[Foreground]
Color=220,220,204
[ForegroundIntense]
Bold=true
Color=220,220,204
[ForegroundFaint]
Color=220,220,204
[General]
Description=Dark Pastels
Opacity=1

View File

@@ -0,0 +1,94 @@
[Background]
Color=0,0,0
[BackgroundIntense]
Color=0,0,0
[BackgroundFaint]
Color=0,0,0
[Color0]
Color=0,0,0
[Color0Intense]
Color=104,104,104
[Color0Faint]
Color=24,24,24
[Color1]
Color=250,75,75
[Color1Intense]
Color=255,84,84
[Color1Faint]
Color=101,25,25
[Color2]
Color=24,178,24
[Color2Intense]
Color=84,255,84
[Color2Faint]
Color=0,101,0
[Color3]
Color=178,104,24
[Color3Intense]
Color=255,255,84
[Color3Faint]
Color=101,74,0
[Color4]
Color=24,24,178
[Color4Intense]
Color=84,84,255
[Color4Faint]
Color=0,0,101
[Color5]
Color=225,30,225
[Color5Intense]
Color=255,84,255
[Color5Faint]
Color=95,5,95
[Color6]
Color=24,178,178
[Color6Intense]
Color=84,255,255
[Color6Faint]
Color=0,101,101
[Color7]
Color=178,178,178
[Color7Intense]
Color=255,255,255
[Color7Faint]
Color=101,101,101
[Foreground]
Color=24,240,24
[ForegroundIntense]
Bold=true
Color=24,240,24
[ForegroundFaint]
Color=18,200,18
[General]
Description=Green on Black
Opacity=1

View File

@@ -0,0 +1,92 @@
[Background]
Color=0,0,0
[BackgroundIntense]
Color=104,104,104
[BackgroundFaint]
Color=0,0,0
[Color0]
Color=0,0,0
[Color0Intense]
Color=104,104,104
[Color0Faint]
Color=24,24,24
[Color1]
Color=178,24,24
[Color1Intense]
Color=255,84,84
[Color1Faint]
Color=101,0,0
[Color2]
Color=24,178,24
[Color2Intense]
Color=84,255,84
[Color2Faint]
Color=0,101,0
[Color3]
Color=178,104,24
[Color3Intense]
Color=255,255,84
[Color3Faint]
Color=101,94,0
[Color4]
Color=24,24,178
[Color4Intense]
Color=84,84,255
[Color4Faint]
Color=0,0,101
[Color5]
Color=178,24,178
[Color5Intense]
Color=255,84,255
[Color5Faint]
Color=101,0,101
[Color6]
Color=24,178,178
[Color6Intense]
Color=84,255,255
[Color6Faint]
Color=0,101,101
[Color7]
Color=178,178,178
[Color7Intense]
Color=255,255,255
[Color7Faint]
Color=101,101,101
[Foreground]
Color=178,178,178
[ForegroundIntense]
Color=255,255,255
[ForegroundFaint]
Color=101,101,101
[General]
Description=Linux Colors

View File

@@ -0,0 +1,94 @@
[Background]
Color=0,0,0
[BackgroundIntense]
Color=0,0,0
[BackgroundFaint]
Color=0,0,0
[Color0]
Color=0,0,0
[Color0Intense]
Color=104,104,104
[Color0Faint]
Color=24,24,24
[Color1]
Color=250,142,8
[Color1Intense]
Color=255,84,84
[Color1Faint]
Color=101,25,0
[Color2]
Color=24,178,24
[Color2Intense]
Color=84,255,84
[Color2Faint]
Color=0,101,0
[Color3]
Color=178,104,24
[Color3Intense]
Color=255,255,84
[Color3Faint]
Color=101,74,0
[Color4]
Color=30,71,152
[Color4Intense]
Color=84,84,255
[Color4Faint]
Color=0,24,102
[Color5]
Color=225,30,225
[Color5Intense]
Color=255,84,255
[Color5Faint]
Color=95,5,95
[Color6]
Color=0,134,223
[Color6Intense]
Color=255,0,4
[Color6Faint]
Color=0,94,163
[Color7]
Color=255,255,255
[Color7Intense]
Color=50,50,50
[Color7Faint]
Color=101,101,101
[Foreground]
Color=255,0,0
[ForegroundIntense]
Bold=true
Color=24,240,24
[ForegroundFaint]
Color=205,0,0
[General]
Description=Red on Black
Opacity=1

View File

@@ -0,0 +1,93 @@
[Color0]
Color=7,54,66
[Color0Intense]
Color=0,43,54
[Color0Faint]
Color=6,48,59
[Color1]
Color=220,50,47
[Color1Intense]
Color=203,75,22
[Color1Faint]
Color=147,33,31
[Color2]
Color=133,153,0
[Color2Intense]
Color=88,110,117
[Color2Faint]
Color=94,106,0
[Color3]
Color=181,137,0
[Color3Intense]
Color=101,123,131
[Color3Faint]
Color=138,103,0
[Color4]
Color=38,139,210
[Color4Intense]
Color=131,148,150
[Color4Faint]
Color=20,77,115
[Color5]
Color=211,54,130
[Color5Intense]
Color=108,113,196
[Color5Faint]
Color=120,30,75
[Color6]
Color=42,161,152
[Color6Intense]
Color=147,161,161
[Color6Faint]
Color=24,94,88
[Color7]
Color=238,232,213
[Color7Intense]
Color=253,246,227
[Color7Faint]
Color=171,167,154
[Background]
Color=0,43,54
[BackgroundIntense]
Color=7,54,66
[BackgroundFaint]
Color=0,43,54
[Foreground]
Color=131,148,150
[ForegroundIntense]
Color=147,161,161
[ForegroundFaint]
Color=106,119,121
[General]
Description=Solarized
Opacity=1

View File

@@ -0,0 +1,93 @@
[Color0]
Color=7,54,66
[Color0Intense]
Color=0,43,54
[Color0Faint]
Color=8,65,80
[Color1]
Color=220,50,47
[Color1Intense]
Color=203,75,22
[Color1Faint]
Color=222,81,81
[Color2]
Color=133,153,0
[Color2Intense]
Color=88,110,117
[Color2Faint]
Color=153,168,39
[Color3]
Color=181,137,0
[Color3Intense]
Color=101,123,131
[Color3Faint]
Color=213,170,49
[Color4]
Color=38,139,210
[Color4Intense]
Color=131,148,150
[Color4Faint]
Color=80,173,226
[Color5]
Color=211,54,130
[Color5Intense]
Color=108,113,196
[Color5Faint]
Color=223,92,158
[Color6]
Color=42,161,152
[Color6Intense]
Color=147,161,161
[Color6Faint]
Color=78,211,200
[Color7]
Color=238,232,213
[Color7Intense]
Color=253,246,227
[Color7Faint]
Color=238,232,213
[Background]
Color=253,246,227
[BackgroundIntense]
Color=238,232,213
[BackgroundFaint]
Color=253,246,227
[Foreground]
Color=101,123,131
[ForegroundIntense]
Color=88,110,117
[ForegroundFaint]
Color=141,172,182
[General]
Description=Solarized Light
Opacity=1

View File

@@ -0,0 +1,94 @@
[Background]
Color=0,0,0
[BackgroundIntense]
Color=0,0,0
[BackgroundFaint]
Color=0,0,0
[Color0]
Color=0,0,0
[Color0Intense]
Color=104,104,104
[Color0Faint]
Color=24,24,24
[Color1]
Color=178,24,24
[Color1Intense]
Color=255,84,84
[Color1Faint]
Color=101,0,0
[Color2]
Color=24,178,24
[Color2Intense]
Color=84,255,84
[Color2Faint]
Color=0,101,0
[Color3]
Color=178,104,24
[Color3Intense]
Color=255,255,84
[Color3Faint]
Color=101,74,0
[Color4]
Color=24,24,178
[Color4Intense]
Color=84,84,255
[Color4Faint]
Color=0,0,101
[Color5]
Color=178,24,178
[Color5Intense]
Color=255,84,255
[Color5Faint]
Color=95,5,95
[Color6]
Color=24,178,178
[Color6Intense]
Color=84,255,255
[Color6Faint]
Color=24,178,178
[Color7]
Color=178,178,178
[Color7Intense]
Color=255,255,255
[Color7Faint]
Color=101,101,101
[Foreground]
Color=255,255,255
[ForegroundIntense]
Bold=true
Color=255,255,255
[ForegroundFaint]
Color=255,255,255
[General]
Description=White on Black
Opacity=1

View File

@@ -0,0 +1,23 @@
{
"name": "0x96f",
"black": "#262427",
"red": "#ff666d",
"green": "#b3e03a",
"yellow": "#ffc739",
"blue": "#00cde8",
"purple": "#a392e8",
"cyan": "#9deaf6",
"white": "#fcfcfa",
"brightBlack": "#545452",
"brightRed": "#ff7e83",
"brightGreen": "#bee55e",
"brightYellow": "#ffd05e",
"brightBlue": "#1bd5eb",
"brightPurple": "#b0a3eb",
"brightCyan": "#acedf8",
"brightWhite": "#fcfcfa",
"background": "#262427",
"foreground": "#fcfcfa",
"cursorColor": "#fcfcfa",
"selectionBackground": "#fcfcfa"
}

View File

@@ -0,0 +1,23 @@
{
"name": "12-bit Rainbow",
"black": "#000000",
"red": "#a03050",
"green": "#40d080",
"yellow": "#e09040",
"blue": "#3060b0",
"purple": "#603090",
"cyan": "#0090c0",
"white": "#dbded8",
"brightBlack": "#685656",
"brightRed": "#c06060",
"brightGreen": "#90d050",
"brightYellow": "#e0d000",
"brightBlue": "#00b0c0",
"brightPurple": "#801070",
"brightCyan": "#20b0c0",
"brightWhite": "#ffffff",
"background": "#040404",
"foreground": "#feffff",
"cursorColor": "#e0d000",
"selectionBackground": "#606060"
}

View File

@@ -0,0 +1,23 @@
{
"name": "3024 Day",
"black": "#090300",
"red": "#db2d20",
"green": "#01a252",
"yellow": "#caba00",
"blue": "#01a0e4",
"purple": "#a16a94",
"cyan": "#8fbece",
"white": "#a5a2a2",
"brightBlack": "#5c5855",
"brightRed": "#dbaec3",
"brightGreen": "#3a3432",
"brightYellow": "#4a4543",
"brightBlue": "#807d7c",
"brightPurple": "#bcbbba",
"brightCyan": "#cdab53",
"brightWhite": "#f7f7f7",
"background": "#f7f7f7",
"foreground": "#4a4543",
"cursorColor": "#4a4543",
"selectionBackground": "#a5a2a2"
}

View File

@@ -0,0 +1,23 @@
{
"name": "3024 Night",
"black": "#090300",
"red": "#db2d20",
"green": "#01a252",
"yellow": "#fded02",
"blue": "#01a0e4",
"purple": "#a16a94",
"cyan": "#b5e4f4",
"white": "#a5a2a2",
"brightBlack": "#5c5855",
"brightRed": "#e8bbd0",
"brightGreen": "#47413f",
"brightYellow": "#4a4543",
"brightBlue": "#807d7c",
"brightPurple": "#d6d5d4",
"brightCyan": "#cdab53",
"brightWhite": "#f7f7f7",
"background": "#090300",
"foreground": "#a5a2a2",
"cursorColor": "#a5a2a2",
"selectionBackground": "#4a4543"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Aardvark Blue",
"black": "#191919",
"red": "#aa342e",
"green": "#4b8c0f",
"yellow": "#dbba00",
"blue": "#1370d3",
"purple": "#c43ac3",
"cyan": "#008eb0",
"white": "#bebebe",
"brightBlack": "#525252",
"brightRed": "#f05b50",
"brightGreen": "#95dc55",
"brightYellow": "#ffe763",
"brightBlue": "#60a4ec",
"brightPurple": "#e26be2",
"brightCyan": "#60b6cb",
"brightWhite": "#f7f7f7",
"background": "#102040",
"foreground": "#dddddd",
"cursorColor": "#007acc",
"selectionBackground": "#bfdbfe"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Abernathy",
"black": "#000000",
"red": "#cd0000",
"green": "#00cd00",
"yellow": "#cdcd00",
"blue": "#1093f5",
"purple": "#cd00cd",
"cyan": "#00cdcd",
"white": "#faebd7",
"brightBlack": "#404040",
"brightRed": "#ff0000",
"brightGreen": "#00ff00",
"brightYellow": "#ffff00",
"brightBlue": "#11b5f6",
"brightPurple": "#ff00ff",
"brightCyan": "#00ffff",
"brightWhite": "#ffffff",
"background": "#111416",
"foreground": "#eeeeec",
"cursorColor": "#bbbbbb",
"selectionBackground": "#eeeeec"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Adventure Time",
"black": "#050404",
"red": "#bd0013",
"green": "#4ab118",
"yellow": "#e7741e",
"blue": "#0f4ac6",
"purple": "#665993",
"cyan": "#70a598",
"white": "#f8dcc0",
"brightBlack": "#4e7cbf",
"brightRed": "#fc5f5a",
"brightGreen": "#9eff6e",
"brightYellow": "#efc11a",
"brightBlue": "#1997c6",
"brightPurple": "#9b5953",
"brightCyan": "#c8faf4",
"brightWhite": "#f6f5fb",
"background": "#1f1d45",
"foreground": "#f8dcc0",
"cursorColor": "#efbf38",
"selectionBackground": "#706b4e"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Adventure",
"black": "#040404",
"red": "#d84a33",
"green": "#5da602",
"yellow": "#eebb6e",
"blue": "#417ab3",
"purple": "#e5c499",
"cyan": "#bdcfe5",
"white": "#dbded8",
"brightBlack": "#685656",
"brightRed": "#d76b42",
"brightGreen": "#99b52c",
"brightYellow": "#ffb670",
"brightBlue": "#97d7ef",
"brightPurple": "#aa7900",
"brightCyan": "#bdcfe5",
"brightWhite": "#e4d5c7",
"background": "#040404",
"foreground": "#feffff",
"cursorColor": "#feffff",
"selectionBackground": "#606060"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Adwaita Dark",
"black": "#241f31",
"red": "#c01c28",
"green": "#2ec27e",
"yellow": "#f5c211",
"blue": "#1e78e4",
"purple": "#9841bb",
"cyan": "#0ab9dc",
"white": "#c0bfbc",
"brightBlack": "#5e5c64",
"brightRed": "#ed333b",
"brightGreen": "#57e389",
"brightYellow": "#f8e45c",
"brightBlue": "#51a1ff",
"brightPurple": "#c061cb",
"brightCyan": "#4fd2fd",
"brightWhite": "#f6f5f4",
"background": "#1d1d20",
"foreground": "#ffffff",
"cursorColor": "#ffffff",
"selectionBackground": "#ffffff"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Adwaita",
"black": "#241f31",
"red": "#c01c28",
"green": "#2ec27e",
"yellow": "#e8b504",
"blue": "#1e78e4",
"purple": "#9841bb",
"cyan": "#0ab9dc",
"white": "#c0bfbc",
"brightBlack": "#5e5c64",
"brightRed": "#ed333b",
"brightGreen": "#4ad67c",
"brightYellow": "#d2be36",
"brightBlue": "#51a1ff",
"brightPurple": "#c061cb",
"brightCyan": "#4fd2fd",
"brightWhite": "#f6f5f4",
"background": "#ffffff",
"foreground": "#000000",
"cursorColor": "#000000",
"selectionBackground": "#c0bfbc"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Afterglow",
"black": "#151515",
"red": "#ac4142",
"green": "#7e8e50",
"yellow": "#e5b567",
"blue": "#6c99bb",
"purple": "#9f4e85",
"cyan": "#7dd6cf",
"white": "#d0d0d0",
"brightBlack": "#505050",
"brightRed": "#ac4142",
"brightGreen": "#7e8e50",
"brightYellow": "#e5b567",
"brightBlue": "#6c99bb",
"brightPurple": "#9f4e85",
"brightCyan": "#7dd6cf",
"brightWhite": "#f5f5f5",
"background": "#212121",
"foreground": "#d0d0d0",
"cursorColor": "#d0d0d0",
"selectionBackground": "#303030"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Aizen Dark",
"black": "#1a1a1a",
"red": "#f08898",
"green": "#a4e09c",
"yellow": "#f5dea4",
"blue": "#84b4f8",
"purple": "#c8a2f4",
"cyan": "#90dcd0",
"white": "#d0d6f0",
"brightBlack": "#444444",
"brightRed": "#f08898",
"brightGreen": "#a4e09c",
"brightYellow": "#f5dea4",
"brightBlue": "#84b4f8",
"brightPurple": "#c8a2f4",
"brightCyan": "#90dcd0",
"brightWhite": "#ffffff",
"background": "#1a1a1a",
"foreground": "#d0d6f0",
"cursorColor": "#f8b080",
"selectionBackground": "#333333"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Aizen Light",
"black": "#f0f2f6",
"red": "#d00c36",
"green": "#3e9e28",
"yellow": "#dd8c1a",
"blue": "#1c64f2",
"purple": "#8636ec",
"cyan": "#159096",
"white": "#4a4d66",
"brightBlack": "#adb2bc",
"brightRed": "#d00c36",
"brightGreen": "#3e9e28",
"brightYellow": "#dd8c1a",
"brightBlue": "#1c64f2",
"brightPurple": "#8636ec",
"brightCyan": "#159096",
"brightWhite": "#4a4d66",
"background": "#f0f2f6",
"foreground": "#4a4d66",
"cursorColor": "#fc6008",
"selectionBackground": "#bdc2cc"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Alabaster",
"black": "#000000",
"red": "#aa3731",
"green": "#448c27",
"yellow": "#cb9000",
"blue": "#325cc0",
"purple": "#7a3e9d",
"cyan": "#0083b2",
"white": "#b7b7b7",
"brightBlack": "#777777",
"brightRed": "#f05050",
"brightGreen": "#60cb00",
"brightYellow": "#f2af50",
"brightBlue": "#007acc",
"brightPurple": "#e64ce6",
"brightCyan": "#00aacb",
"brightWhite": "#f7f7f7",
"background": "#f7f7f7",
"foreground": "#000000",
"cursorColor": "#007acc",
"selectionBackground": "#bfdbfe"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Alien Blood",
"black": "#112616",
"red": "#7f2b27",
"green": "#2f7e25",
"yellow": "#717f24",
"blue": "#2f6a7f",
"purple": "#47587f",
"cyan": "#327f77",
"white": "#647d75",
"brightBlack": "#3c4812",
"brightRed": "#e08009",
"brightGreen": "#18e000",
"brightYellow": "#bde000",
"brightBlue": "#00aae0",
"brightPurple": "#0058e0",
"brightCyan": "#00e0c4",
"brightWhite": "#73fa91",
"background": "#0f1610",
"foreground": "#637d75",
"cursorColor": "#73fa91",
"selectionBackground": "#1d4125"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Andromeda",
"black": "#000000",
"red": "#cd3131",
"green": "#05bc79",
"yellow": "#e5e512",
"blue": "#2472c8",
"purple": "#bc3fbc",
"cyan": "#0fa8cd",
"white": "#e5e5e5",
"brightBlack": "#666666",
"brightRed": "#cd3131",
"brightGreen": "#05bc79",
"brightYellow": "#e5e512",
"brightBlue": "#2472c8",
"brightPurple": "#bc3fbc",
"brightCyan": "#0fa8cd",
"brightWhite": "#e5e5e5",
"background": "#262a33",
"foreground": "#e5e5e5",
"cursorColor": "#f8f8f0",
"selectionBackground": "#5a5c62"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Apple Classic",
"black": "#000000",
"red": "#c91b00",
"green": "#00c200",
"yellow": "#c7c400",
"blue": "#1c3fe1",
"purple": "#ca30c7",
"cyan": "#00c5c7",
"white": "#c7c7c7",
"brightBlack": "#686868",
"brightRed": "#ff6e67",
"brightGreen": "#5ffa68",
"brightYellow": "#fffc67",
"brightBlue": "#6871ff",
"brightPurple": "#ff77ff",
"brightCyan": "#60fdff",
"brightWhite": "#ffffff",
"background": "#2c2b2b",
"foreground": "#d5a200",
"cursorColor": "#c7c7c7",
"selectionBackground": "#6b5b02"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Apple System Colors Light",
"black": "#1a1a1a",
"red": "#cc372e",
"green": "#26a439",
"yellow": "#cdac08",
"blue": "#0869cb",
"purple": "#9647bf",
"cyan": "#479ec2",
"white": "#98989d",
"brightBlack": "#464646",
"brightRed": "#ff453a",
"brightGreen": "#32d74b",
"brightYellow": "#e5bc00",
"brightBlue": "#0a84ff",
"brightPurple": "#bf5af2",
"brightCyan": "#69c9f2",
"brightWhite": "#ffffff",
"background": "#feffff",
"foreground": "#000000",
"cursorColor": "#98989d",
"selectionBackground": "#abd8ff"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Apple System Colors",
"black": "#1a1a1a",
"red": "#cc372e",
"green": "#26a439",
"yellow": "#cdac08",
"blue": "#0869cb",
"purple": "#9647bf",
"cyan": "#479ec2",
"white": "#98989d",
"brightBlack": "#464646",
"brightRed": "#ff453a",
"brightGreen": "#32d74b",
"brightYellow": "#ffd60a",
"brightBlue": "#0a84ff",
"brightPurple": "#bf5af2",
"brightCyan": "#76d6ff",
"brightWhite": "#ffffff",
"background": "#1e1e1e",
"foreground": "#ffffff",
"cursorColor": "#98989d",
"selectionBackground": "#3f638b"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Arcoiris",
"black": "#333333",
"red": "#da2700",
"green": "#12c258",
"yellow": "#ffc656",
"blue": "#518bfc",
"purple": "#e37bd9",
"cyan": "#63fad5",
"white": "#bab2b2",
"brightBlack": "#777777",
"brightRed": "#ffb9b9",
"brightGreen": "#e3f6aa",
"brightYellow": "#ffddaa",
"brightBlue": "#b3e8f3",
"brightPurple": "#cbbaf9",
"brightCyan": "#bcffc7",
"brightWhite": "#efefef",
"background": "#201f1e",
"foreground": "#eee4d9",
"cursorColor": "#872929",
"selectionBackground": "#25524a"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Ardoise",
"black": "#2c2c2c",
"red": "#d3322d",
"green": "#588b35",
"yellow": "#fca93a",
"blue": "#2465c2",
"purple": "#7332b4",
"cyan": "#64e1b8",
"white": "#f7f7f7",
"brightBlack": "#535353",
"brightRed": "#fa5852",
"brightGreen": "#8dc252",
"brightYellow": "#ffea51",
"brightBlue": "#6ab5f8",
"brightPurple": "#be68ca",
"brightCyan": "#89ffdb",
"brightWhite": "#fefefe",
"background": "#1e1e1e",
"foreground": "#eaeaea",
"cursorColor": "#f7f7f7",
"selectionBackground": "#46515e"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Argonaut",
"black": "#232323",
"red": "#ff000f",
"green": "#8ce10b",
"yellow": "#ffb900",
"blue": "#008df8",
"purple": "#6d43a6",
"cyan": "#00d8eb",
"white": "#ffffff",
"brightBlack": "#444444",
"brightRed": "#ff2740",
"brightGreen": "#abe15b",
"brightYellow": "#ffd242",
"brightBlue": "#0092ff",
"brightPurple": "#9a5feb",
"brightCyan": "#67fff0",
"brightWhite": "#ffffff",
"background": "#0e1019",
"foreground": "#fffaf4",
"cursorColor": "#ff0018",
"selectionBackground": "#002a3b"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Arthur",
"black": "#3d352a",
"red": "#cd5c5c",
"green": "#86af80",
"yellow": "#e8ae5b",
"blue": "#6495ed",
"purple": "#deb887",
"cyan": "#b0c4de",
"white": "#bbaa99",
"brightBlack": "#554444",
"brightRed": "#cc5533",
"brightGreen": "#88aa22",
"brightYellow": "#ffa75d",
"brightBlue": "#87ceeb",
"brightPurple": "#996600",
"brightCyan": "#b0c4de",
"brightWhite": "#ddccbb",
"background": "#1c1c1c",
"foreground": "#ddeedd",
"cursorColor": "#e2bbef",
"selectionBackground": "#4d4d4d"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Atelier Sulphurpool",
"black": "#202746",
"red": "#c94922",
"green": "#ac9739",
"yellow": "#c08b30",
"blue": "#3d8fd1",
"purple": "#6679cc",
"cyan": "#22a2c9",
"white": "#979db4",
"brightBlack": "#6b7394",
"brightRed": "#c76b29",
"brightGreen": "#4f587c",
"brightYellow": "#5e6687",
"brightBlue": "#898ea4",
"brightPurple": "#dfe2f1",
"brightCyan": "#9c637a",
"brightWhite": "#f5f7ff",
"background": "#202746",
"foreground": "#979db4",
"cursorColor": "#979db4",
"selectionBackground": "#5e6687"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Atom One Dark",
"black": "#21252b",
"red": "#e06c75",
"green": "#98c379",
"yellow": "#e5c07b",
"blue": "#61afef",
"purple": "#c678dd",
"cyan": "#56b6c2",
"white": "#abb2bf",
"brightBlack": "#767676",
"brightRed": "#e06c75",
"brightGreen": "#98c379",
"brightYellow": "#e5c07b",
"brightBlue": "#61afef",
"brightPurple": "#c678dd",
"brightCyan": "#56b6c2",
"brightWhite": "#abb2bf",
"background": "#21252b",
"foreground": "#abb2bf",
"cursorColor": "#abb2bf",
"selectionBackground": "#323844"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Atom One Light",
"black": "#000000",
"red": "#de3e35",
"green": "#3f953a",
"yellow": "#d2b67c",
"blue": "#2f5af3",
"purple": "#950095",
"cyan": "#3f953a",
"white": "#bbbbbb",
"brightBlack": "#000000",
"brightRed": "#de3e35",
"brightGreen": "#3f953a",
"brightYellow": "#d2b67c",
"brightBlue": "#2f5af3",
"brightPurple": "#a00095",
"brightCyan": "#3f953a",
"brightWhite": "#ffffff",
"background": "#f9f9f9",
"foreground": "#2a2c33",
"cursorColor": "#bbbbbb",
"selectionBackground": "#ededed"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Atom",
"black": "#000000",
"red": "#fd5ff1",
"green": "#87c38a",
"yellow": "#ffd7b1",
"blue": "#85befd",
"purple": "#b9b6fc",
"cyan": "#85befd",
"white": "#e0e0e0",
"brightBlack": "#4c4c4c",
"brightRed": "#fd5ff1",
"brightGreen": "#94fa36",
"brightYellow": "#f5ffa8",
"brightBlue": "#96cbfe",
"brightPurple": "#b9b6fc",
"brightCyan": "#85befd",
"brightWhite": "#e0e0e0",
"background": "#161719",
"foreground": "#c5c8c6",
"cursorColor": "#d0d0d0",
"selectionBackground": "#444444"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Aura",
"black": "#110f18",
"red": "#ff6767",
"green": "#61ffca",
"yellow": "#ffca85",
"blue": "#a277ff",
"purple": "#a277ff",
"cyan": "#61ffca",
"white": "#edecee",
"brightBlack": "#4d4d4d",
"brightRed": "#ffca85",
"brightGreen": "#a277ff",
"brightYellow": "#ffca85",
"brightBlue": "#a277ff",
"brightPurple": "#a277ff",
"brightCyan": "#61ffca",
"brightWhite": "#edecee",
"background": "#15141b",
"foreground": "#edecee",
"cursorColor": "#a277ff",
"selectionBackground": "#a277ff"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Aurora",
"black": "#23262e",
"red": "#f0266f",
"green": "#8fd46d",
"yellow": "#ffe66d",
"blue": "#102ee4",
"purple": "#ee5d43",
"cyan": "#03d6b8",
"white": "#c74ded",
"brightBlack": "#4f545e",
"brightRed": "#f92672",
"brightGreen": "#8fd46d",
"brightYellow": "#ffe66d",
"brightBlue": "#03d6b8",
"brightPurple": "#ee5d43",
"brightCyan": "#03d6b8",
"brightWhite": "#c74ded",
"background": "#23262e",
"foreground": "#ffca28",
"cursorColor": "#ee5d43",
"selectionBackground": "#292e38"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Ayu Light",
"black": "#000000",
"red": "#ea6c6d",
"green": "#6cbf43",
"yellow": "#eca944",
"blue": "#3199e1",
"purple": "#9e75c7",
"cyan": "#46ba94",
"white": "#bababa",
"brightBlack": "#686868",
"brightRed": "#f07171",
"brightGreen": "#86b300",
"brightYellow": "#f2ae49",
"brightBlue": "#399ee6",
"brightPurple": "#a37acc",
"brightCyan": "#4cbf99",
"brightWhite": "#d1d1d1",
"background": "#f8f9fa",
"foreground": "#5c6166",
"cursorColor": "#ffaa33",
"selectionBackground": "#035bd6"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Ayu Mirage",
"black": "#171b24",
"red": "#ed8274",
"green": "#87d96c",
"yellow": "#facc6e",
"blue": "#6dcbfa",
"purple": "#dabafa",
"cyan": "#90e1c6",
"white": "#c7c7c7",
"brightBlack": "#686868",
"brightRed": "#f28779",
"brightGreen": "#d5ff80",
"brightYellow": "#ffd173",
"brightBlue": "#73d0ff",
"brightPurple": "#dfbfff",
"brightCyan": "#95e6cb",
"brightWhite": "#ffffff",
"background": "#1f2430",
"foreground": "#cccac2",
"cursorColor": "#ffcc66",
"selectionBackground": "#409fff"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Ayu",
"black": "#11151c",
"red": "#ea6c73",
"green": "#7fd962",
"yellow": "#f9af4f",
"blue": "#53bdfa",
"purple": "#cda1fa",
"cyan": "#90e1c6",
"white": "#c7c7c7",
"brightBlack": "#686868",
"brightRed": "#f07178",
"brightGreen": "#aad94c",
"brightYellow": "#ffb454",
"brightBlue": "#59c2ff",
"brightPurple": "#d2a6ff",
"brightCyan": "#95e6cb",
"brightWhite": "#ffffff",
"background": "#0b0e14",
"foreground": "#bfbdb6",
"cursorColor": "#e6b450",
"selectionBackground": "#409fff"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Banana Blueberry",
"black": "#17141f",
"red": "#ff6b7f",
"green": "#00bd9c",
"yellow": "#e6c62f",
"blue": "#22e8df",
"purple": "#dc396a",
"cyan": "#56b6c2",
"white": "#f1f1f1",
"brightBlack": "#495162",
"brightRed": "#fe9ea1",
"brightGreen": "#98c379",
"brightYellow": "#f9e46b",
"brightBlue": "#91fff4",
"brightPurple": "#da70d6",
"brightCyan": "#bcf3ff",
"brightWhite": "#ffffff",
"background": "#191323",
"foreground": "#cccccc",
"cursorColor": "#e07d13",
"selectionBackground": "#220525"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Batman",
"black": "#1b1d1e",
"red": "#e6dc44",
"green": "#c8be46",
"yellow": "#f4fd22",
"blue": "#737174",
"purple": "#747271",
"cyan": "#62605f",
"white": "#c6c5bf",
"brightBlack": "#505354",
"brightRed": "#fff78e",
"brightGreen": "#fff27d",
"brightYellow": "#feed6c",
"brightBlue": "#919495",
"brightPurple": "#9a9a9d",
"brightCyan": "#a3a3a6",
"brightWhite": "#dadbd6",
"background": "#1b1d1e",
"foreground": "#6f6f6f",
"cursorColor": "#fcef0c",
"selectionBackground": "#4d504c"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Belafonte Day",
"black": "#20111b",
"red": "#be100e",
"green": "#858162",
"yellow": "#d08b30",
"blue": "#426a79",
"purple": "#97522c",
"cyan": "#989a9c",
"white": "#968c83",
"brightBlack": "#5e5252",
"brightRed": "#be100e",
"brightGreen": "#858162",
"brightYellow": "#d08b30",
"brightBlue": "#426a79",
"brightPurple": "#97522c",
"brightCyan": "#989a9c",
"brightWhite": "#d5ccba",
"background": "#d5ccba",
"foreground": "#45373c",
"cursorColor": "#45373c",
"selectionBackground": "#968c83"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Belafonte Night",
"black": "#20111b",
"red": "#be100e",
"green": "#858162",
"yellow": "#eaa549",
"blue": "#426a79",
"purple": "#97522c",
"cyan": "#989a9c",
"white": "#968c83",
"brightBlack": "#5e5252",
"brightRed": "#be100e",
"brightGreen": "#858162",
"brightYellow": "#eaa549",
"brightBlue": "#426a79",
"brightPurple": "#97522c",
"brightCyan": "#989a9c",
"brightWhite": "#d5ccba",
"background": "#20111b",
"foreground": "#968c83",
"cursorColor": "#968c83",
"selectionBackground": "#45373c"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Birds Of Paradise",
"black": "#573d26",
"red": "#be2d26",
"green": "#6ba18a",
"yellow": "#e99d2a",
"blue": "#5a86ad",
"purple": "#ac80a6",
"cyan": "#74a6ad",
"white": "#e0dbb7",
"brightBlack": "#9b6c4a",
"brightRed": "#e84627",
"brightGreen": "#95d8ba",
"brightYellow": "#d0d150",
"brightBlue": "#b8d3ed",
"brightPurple": "#d19ecb",
"brightCyan": "#93cfd7",
"brightWhite": "#fff9d5",
"background": "#2a1f1d",
"foreground": "#e0dbb7",
"cursorColor": "#644a33",
"selectionBackground": "#563c27"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal (Bathory)",
"black": "#000000",
"red": "#5f8787",
"green": "#fbcb97",
"yellow": "#e78a53",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#5f8787",
"brightGreen": "#fbcb97",
"brightYellow": "#e78a53",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal (Burzum)",
"black": "#000000",
"red": "#5f8787",
"green": "#ddeecc",
"yellow": "#99bbaa",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#5f8787",
"brightGreen": "#ddeecc",
"brightYellow": "#99bbaa",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal (Dark Funeral)",
"black": "#000000",
"red": "#5f8787",
"green": "#d0dfee",
"yellow": "#5f81a5",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#5f8787",
"brightGreen": "#d0dfee",
"brightYellow": "#5f81a5",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal (Gorgoroth)",
"black": "#000000",
"red": "#5f8787",
"green": "#9b8d7f",
"yellow": "#8c7f70",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#5f8787",
"brightGreen": "#9b8d7f",
"brightYellow": "#8c7f70",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal (Immortal)",
"black": "#000000",
"red": "#5f8787",
"green": "#7799bb",
"yellow": "#556677",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#5f8787",
"brightGreen": "#7799bb",
"brightYellow": "#556677",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal (Khold)",
"black": "#000000",
"red": "#5f8787",
"green": "#eceee3",
"yellow": "#974b46",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#5f8787",
"brightGreen": "#eceee3",
"brightYellow": "#974b46",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal (Marduk)",
"black": "#000000",
"red": "#5f8787",
"green": "#a5aaa7",
"yellow": "#626b67",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#5f8787",
"brightGreen": "#a5aaa7",
"brightYellow": "#626b67",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal (Mayhem)",
"black": "#000000",
"red": "#5f8787",
"green": "#f3ecd4",
"yellow": "#eecc6c",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#5f8787",
"brightGreen": "#f3ecd4",
"brightYellow": "#eecc6c",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal (Nile)",
"black": "#000000",
"red": "#5f8787",
"green": "#aa9988",
"yellow": "#777755",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#5f8787",
"brightGreen": "#aa9988",
"brightYellow": "#777755",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal (Venom)",
"black": "#000000",
"red": "#5f8787",
"green": "#f8f7f2",
"yellow": "#79241f",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#5f8787",
"brightGreen": "#f8f7f2",
"brightYellow": "#79241f",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Black Metal",
"black": "#000000",
"red": "#486e6f",
"green": "#dd9999",
"yellow": "#a06666",
"blue": "#888888",
"purple": "#999999",
"cyan": "#aaaaaa",
"white": "#c1c1c1",
"brightBlack": "#404040",
"brightRed": "#486e6f",
"brightGreen": "#dd9999",
"brightYellow": "#a06666",
"brightBlue": "#888888",
"brightPurple": "#999999",
"brightCyan": "#aaaaaa",
"brightWhite": "#c1c1c1",
"background": "#000000",
"foreground": "#c1c1c1",
"cursorColor": "#c1c1c1",
"selectionBackground": "#c1c1c1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Blazer",
"black": "#000000",
"red": "#b87a7a",
"green": "#7ab87a",
"yellow": "#b8b87a",
"blue": "#7a7ab8",
"purple": "#b87ab8",
"cyan": "#7ab8b8",
"white": "#d9d9d9",
"brightBlack": "#4c4c4c",
"brightRed": "#dbbdbd",
"brightGreen": "#bddbbd",
"brightYellow": "#dbdbbd",
"brightBlue": "#bdbddb",
"brightPurple": "#dbbddb",
"brightCyan": "#bddbdb",
"brightWhite": "#ffffff",
"background": "#0d1926",
"foreground": "#d9e6f2",
"cursorColor": "#d9e6f2",
"selectionBackground": "#c1ddff"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Blue Berry Pie",
"black": "#0a4c62",
"red": "#99246e",
"green": "#5cb1b3",
"yellow": "#eab9a8",
"blue": "#90a5bd",
"purple": "#9d54a7",
"cyan": "#7e83cc",
"white": "#f0e8d6",
"brightBlack": "#463c5d",
"brightRed": "#c87272",
"brightGreen": "#0a6c7e",
"brightYellow": "#7a3188",
"brightBlue": "#5f3d63",
"brightPurple": "#bc94b7",
"brightCyan": "#5e6071",
"brightWhite": "#0a6c7e",
"background": "#1c0c28",
"foreground": "#babab9",
"cursorColor": "#fcfad6",
"selectionBackground": "#606060"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Blue Dolphin",
"black": "#292d3e",
"red": "#ff8288",
"green": "#b4e88d",
"yellow": "#f4d69f",
"blue": "#82aaff",
"purple": "#e9c1ff",
"cyan": "#89ebff",
"white": "#d0d0d0",
"brightBlack": "#9094a4",
"brightRed": "#ff8b92",
"brightGreen": "#ddffa7",
"brightYellow": "#ffe585",
"brightBlue": "#9cc4ff",
"brightPurple": "#ddb0f6",
"brightCyan": "#a3f7ff",
"brightWhite": "#ffffff",
"background": "#006984",
"foreground": "#c5f2ff",
"cursorColor": "#ffcc00",
"selectionBackground": "#2baeca"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Blue Matrix",
"black": "#101116",
"red": "#ff5680",
"green": "#00ff9c",
"yellow": "#fffc58",
"blue": "#00b0ff",
"purple": "#d57bff",
"cyan": "#76c1ff",
"white": "#c7c7c7",
"brightBlack": "#686868",
"brightRed": "#ff6e67",
"brightGreen": "#5ffa68",
"brightYellow": "#fffc67",
"brightBlue": "#6871ff",
"brightPurple": "#d682ec",
"brightCyan": "#60fdff",
"brightWhite": "#ffffff",
"background": "#101116",
"foreground": "#00a2ff",
"cursorColor": "#76ff9f",
"selectionBackground": "#c1deff"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Bluloco Dark",
"black": "#41444d",
"red": "#fc2f52",
"green": "#25a45c",
"yellow": "#ff936a",
"blue": "#3476ff",
"purple": "#7a82da",
"cyan": "#4483aa",
"white": "#cdd4e0",
"brightBlack": "#8f9aae",
"brightRed": "#ff6480",
"brightGreen": "#3fc56b",
"brightYellow": "#f9c859",
"brightBlue": "#10b1fe",
"brightPurple": "#ff78f8",
"brightCyan": "#5fb9bc",
"brightWhite": "#ffffff",
"background": "#282c34",
"foreground": "#b9c0cb",
"cursorColor": "#ffcc00",
"selectionBackground": "#b9c0ca"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Bluloco Light",
"black": "#373a41",
"red": "#d52753",
"green": "#23974a",
"yellow": "#df631c",
"blue": "#275fe4",
"purple": "#823ff1",
"cyan": "#27618d",
"white": "#babbc2",
"brightBlack": "#676a77",
"brightRed": "#ff6480",
"brightGreen": "#3cbc66",
"brightYellow": "#c5a332",
"brightBlue": "#0099e1",
"brightPurple": "#ce33c0",
"brightCyan": "#6d93bb",
"brightWhite": "#d3d3d3",
"background": "#f9f9f9",
"foreground": "#373a41",
"cursorColor": "#f32759",
"selectionBackground": "#daf0ff"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Borland",
"black": "#4f4f4f",
"red": "#ff6c60",
"green": "#a8ff60",
"yellow": "#ffffb6",
"blue": "#96cbfe",
"purple": "#ff73fd",
"cyan": "#c6c5fe",
"white": "#eeeeee",
"brightBlack": "#7c7c7c",
"brightRed": "#ffb6b0",
"brightGreen": "#ceffac",
"brightYellow": "#ffffcc",
"brightBlue": "#b5dcff",
"brightPurple": "#ff9cfe",
"brightCyan": "#dfdffe",
"brightWhite": "#ffffff",
"background": "#0000a4",
"foreground": "#ffff4e",
"cursorColor": "#ffa560",
"selectionBackground": "#a4a4a4"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Box",
"black": "#000000",
"red": "#cc0403",
"green": "#19cb00",
"yellow": "#cecb00",
"blue": "#0d73cc",
"purple": "#cb1ed1",
"cyan": "#0dcdcd",
"white": "#dddddd",
"brightBlack": "#767676",
"brightRed": "#f2201f",
"brightGreen": "#23fd00",
"brightYellow": "#fffd00",
"brightBlue": "#1a8fff",
"brightPurple": "#fd28ff",
"brightCyan": "#14ffff",
"brightWhite": "#ffffff",
"background": "#141d2b",
"foreground": "#9fef00",
"cursorColor": "#9fef00",
"selectionBackground": "#a4b1cd"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Breadog",
"black": "#362c24",
"red": "#b10b00",
"green": "#007232",
"yellow": "#8b4c00",
"blue": "#005cb4",
"purple": "#9b0097",
"cyan": "#006a78",
"white": "#baa99d",
"brightBlack": "#514337",
"brightRed": "#de1100",
"brightGreen": "#008f40",
"brightYellow": "#ae6000",
"brightBlue": "#0074e1",
"brightPurple": "#c300bd",
"brightCyan": "#008697",
"brightWhite": "#eae1da",
"background": "#f1ebe6",
"foreground": "#362c24",
"cursorColor": "#362c24",
"selectionBackground": "#362c24"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Breeze",
"black": "#31363b",
"red": "#ed1515",
"green": "#11d116",
"yellow": "#f67400",
"blue": "#1d99f3",
"purple": "#9b59b6",
"cyan": "#1abc9c",
"white": "#eff0f1",
"brightBlack": "#7f8c8d",
"brightRed": "#c0392b",
"brightGreen": "#1cdc9a",
"brightYellow": "#fdbc4b",
"brightBlue": "#3daee9",
"brightPurple": "#8e44ad",
"brightCyan": "#16a085",
"brightWhite": "#fcfcfc",
"background": "#31363b",
"foreground": "#eff0f1",
"cursorColor": "#eff0f1",
"selectionBackground": "#eff0f1"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Bright Lights",
"black": "#191919",
"red": "#ff355b",
"green": "#b7e876",
"yellow": "#ffc251",
"blue": "#76d4ff",
"purple": "#ba76e7",
"cyan": "#6cbfb5",
"white": "#c2c8d7",
"brightBlack": "#4c4c4c",
"brightRed": "#ff355b",
"brightGreen": "#b7e876",
"brightYellow": "#ffc251",
"brightBlue": "#76d5ff",
"brightPurple": "#ba76e7",
"brightCyan": "#6cbfb5",
"brightWhite": "#c2c8d7",
"background": "#191919",
"foreground": "#b3c9d7",
"cursorColor": "#f34b00",
"selectionBackground": "#b3c9d7"
}

View File

@@ -0,0 +1,23 @@
{
"name": "Broadcast",
"black": "#000000",
"red": "#da4939",
"green": "#519f50",
"yellow": "#ffd24a",
"blue": "#6d9cbe",
"purple": "#d0d0ff",
"cyan": "#6e9cbe",
"white": "#ffffff",
"brightBlack": "#585858",
"brightRed": "#ff7b6b",
"brightGreen": "#83d182",
"brightYellow": "#ffff7c",
"brightBlue": "#9fcef0",
"brightPurple": "#ffffff",
"brightCyan": "#a0cef0",
"brightWhite": "#ffffff",
"background": "#2b2b2b",
"foreground": "#e6e1dc",
"cursorColor": "#ffffff",
"selectionBackground": "#5a647e"
}

Some files were not shown because too many files have changed in this diff Show More