├── .android └── .gitkeep ├── src-tauri ├── src │ ├── core │ │ ├── auth_events.rs │ │ ├── mod.rs │ │ └── title_detector.rs │ ├── models │ │ ├── recording │ │ │ ├── mod.rs │ │ │ └── recording.rs │ │ ├── system │ │ │ ├── mod.rs │ │ │ └── info.rs │ │ ├── auth │ │ │ ├── mod.rs │ │ │ └── requests.rs │ │ ├── history │ │ │ ├── mod.rs │ │ │ └── history.rs │ │ ├── sftp │ │ │ ├── mod.rs │ │ │ ├── search.rs │ │ │ ├── file_entry.rs │ │ │ ├── error.rs │ │ │ ├── transfer.rs │ │ │ └── sync.rs │ │ ├── mod.rs │ │ ├── saved_command │ │ │ ├── mod.rs │ │ │ ├── group.rs │ │ │ └── command.rs │ │ ├── terminal │ │ │ ├── mod.rs │ │ │ ├── profile.rs │ │ │ └── requests.rs │ │ ├── sync │ │ │ ├── mod.rs │ │ │ ├── stats.rs │ │ │ ├── conflict.rs │ │ │ ├── log.rs │ │ │ └── progress.rs │ │ ├── ssh │ │ │ └── mod.rs │ │ ├── buffer.rs │ │ └── base.rs │ ├── database │ │ ├── sync │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── providers │ │ │ └── mod.rs │ │ ├── encryption │ │ │ ├── mod.rs │ │ │ ├── external_db.rs │ │ │ └── aes.rs │ │ ├── config.rs │ │ └── traits_sync.rs │ ├── services │ │ ├── recording │ │ │ └── mod.rs │ │ ├── saved_command │ │ │ └── mod.rs │ │ ├── sftp │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── ssh │ │ │ └── connection_pool.rs │ │ └── sync │ │ │ └── serializer.rs │ ├── main.rs │ ├── commands │ │ ├── mod.rs │ │ ├── database │ │ │ ├── mod.rs │ │ │ └── common.rs │ │ ├── history.rs │ │ ├── terminal_profile.rs │ │ ├── auth_events.rs │ │ └── buffer.rs │ └── setup.rs ├── build.rs ├── icons │ ├── 32x32.png │ ├── 64x64.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square310x310Logo.png │ ├── ios │ │ ├── AppIcon-20x20@1x.png │ │ ├── AppIcon-20x20@2x.png │ │ ├── AppIcon-20x20@3x.png │ │ ├── AppIcon-29x29@1x.png │ │ ├── AppIcon-29x29@2x.png │ │ ├── AppIcon-29x29@3x.png │ │ ├── AppIcon-40x40@1x.png │ │ ├── AppIcon-40x40@2x.png │ │ ├── AppIcon-40x40@3x.png │ │ ├── AppIcon-512@2x.png │ │ ├── AppIcon-60x60@2x.png │ │ ├── AppIcon-60x60@3x.png │ │ ├── AppIcon-76x76@1x.png │ │ ├── AppIcon-76x76@2x.png │ │ ├── AppIcon-20x20@2x-1.png │ │ ├── AppIcon-29x29@2x-1.png │ │ ├── AppIcon-40x40@2x-1.png │ │ └── AppIcon-83.5x83.5@2x.png │ └── android │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ └── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png ├── .gitignore ├── capabilities │ ├── desktop.json │ ├── clipboard.json │ └── default.json ├── Cargo.toml └── tauri.conf.json ├── app-icon.png ├── src ├── core │ ├── index.ts │ ├── terminal │ │ └── index.ts │ └── performance │ │ └── index.ts ├── assets │ ├── images │ │ ├── logo_250.png │ │ └── logo_500.png │ ├── fonts │ │ └── FiraCodeNerdFont.ttf │ └── css │ │ ├── fonts.css │ │ └── main.css ├── services │ ├── dashboard.ts │ ├── settings.ts │ ├── history.ts │ ├── recording.ts │ ├── tunnel.ts │ ├── buffer.ts │ ├── sshKey.ts │ ├── savedCommand.ts │ └── auth.ts ├── vite-env.d.ts ├── components │ ├── terminal-profiles │ │ ├── TerminalProfileManager.vue │ │ └── TerminalProfileItem.vue │ ├── recording │ │ ├── RecordingsModal.vue │ │ ├── RecordingsManager.vue │ │ └── RecordingControls.vue │ ├── CommandPaletteManager.vue │ ├── auth │ │ ├── MasterPasswordManager.vue │ │ ├── MasterPasswordUnlock.vue │ │ └── PasswordConfirmModal.vue │ ├── ui │ │ ├── SkeletonText.vue │ │ ├── EmptyState.vue │ │ ├── SkeletonList.vue │ │ ├── Form.vue │ │ ├── NavigationTabs.vue │ │ ├── CommandPreview.vue │ │ ├── Badge.vue │ │ ├── MessageContainer.vue │ │ ├── PasswordStrength.vue │ │ ├── EnvVarEditor.vue │ │ ├── SkeletonLoader.vue │ │ └── Checkbox.vue │ ├── tunnels │ │ ├── TunnelManager.vue │ │ └── TunnelStatusIndicator.vue │ ├── saved-commands │ │ └── SavedCommandManager.vue │ ├── sync │ │ └── SyncStatusIndicator.vue │ ├── settings │ │ └── SettingsManager.vue │ ├── Workspace.vue │ ├── ssh-profiles │ │ ├── SSHProfileManager.vue │ │ ├── SSHConfigHostItem.vue │ │ ├── RecentConnectionItem.vue │ │ └── SSHProfileItem.vue │ ├── sftp │ │ ├── FileDeleteModal.vue │ │ ├── CreateFileModal.vue │ │ ├── CreateDirectoryModal.vue │ │ └── FileRenameModal.vue │ └── history │ │ └── HistoryItem.vue ├── types │ ├── splitpanes.d.ts │ ├── form.ts │ ├── history.ts │ ├── terminalProfile.ts │ ├── recording.ts │ ├── asciinema-player.d.ts │ ├── overlay.ts │ ├── system.ts │ ├── tunnel.ts │ ├── auth.ts │ └── savedCommand.ts ├── stores │ └── viewState.ts ├── utils │ ├── terminalTheme.ts │ └── validators.ts ├── main.ts └── composables │ ├── useWindowSize.ts │ ├── useKeyboardShortcuts.ts │ ├── useFormStyles.ts │ └── useDebounce.ts ├── public ├── favicon.ico └── screenshots │ ├── Dashboard.png │ └── MainInterface.png ├── .github └── FUNDING.yml ├── .sonarlint └── connectedMode.json ├── .vscode └── extensions.json ├── tsconfig.node.json ├── .editorconfig ├── index.html ├── .gitignore ├── tsconfig.json ├── vite.config.ts ├── LICENSE ├── package.json └── scripts ├── generate-keystore.sh └── create-version.sh /.android/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/src/core/auth_events.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/app-icon.png -------------------------------------------------------------------------------- /src-tauri/src/models/recording/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod recording; 2 | pub use recording::*; 3 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./performance"; 2 | export * from "./terminal"; 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/core/terminal/index.ts: -------------------------------------------------------------------------------- 1 | export { TerminalBufferManager } from "./TerminalBufferManager"; 2 | -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/64x64.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/database/sync/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod manager; 2 | pub mod scheduler; 3 | pub mod strategies; 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/src/services/recording/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod recorder; 2 | 3 | pub use recorder::SessionRecorder; 4 | -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/src/services/saved_command/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod service; 2 | 3 | pub use service::SavedCommandService; 4 | -------------------------------------------------------------------------------- /src/assets/images/logo_250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src/assets/images/logo_250.png -------------------------------------------------------------------------------- /src/assets/images/logo_500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src/assets/images/logo_500.png -------------------------------------------------------------------------------- /public/screenshots/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/public/screenshots/Dashboard.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: klpod221 2 | open_collective: kerminal 3 | custom: ["https://www.buymeacoffee.com/klpod221"] 4 | -------------------------------------------------------------------------------- /public/screenshots/MainInterface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/public/screenshots/MainInterface.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_session_manager; 2 | pub mod proxy; 3 | pub mod terminal; 4 | pub mod title_detector; 5 | -------------------------------------------------------------------------------- /src/assets/fonts/FiraCodeNerdFont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src/assets/fonts/FiraCodeNerdFont.ttf -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /src-tauri/src/models/system/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod info; 2 | 3 | pub use info::{CPUInfo, ComponentInfo, DiskInfo, NetworkInterface, SystemInfo}; 4 | -------------------------------------------------------------------------------- /.sonarlint/connectedMode.json: -------------------------------------------------------------------------------- 1 | { 2 | "sonarCloudOrganization": "klpod221", 3 | "projectKey": "klpod221_kerminal", 4 | "region": "EU" 5 | } -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | 3 | fn main() { 4 | kerminal::main(); 5 | } 6 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "tauri-apps.tauri-vscode", 5 | "rust-lang.rust-analyzer" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/src/services/sftp/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod channel_stream; 2 | pub mod service; 3 | pub mod sync; 4 | pub mod transfer; 5 | 6 | pub use service::SFTPService; 7 | -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klpod221/kerminal/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src/assets/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "FiraCode Nerd Font"; 3 | 4 | src: url("../fonts/FiraCodeNerdFont.ttf"); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | -------------------------------------------------------------------------------- /src/services/dashboard.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./api"; 2 | 3 | export async function getSystemInfo() { 4 | const systemInfo = await api.callRaw("get_system_info"); 5 | return systemInfo; 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/src/models/auth/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod device; 2 | pub mod requests; 3 | 4 | pub use device::{Device, DeviceType, OsInfo}; 5 | pub use requests::{ChangeMasterPasswordRequest, VerifyMasterPasswordRequest}; 6 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | -------------------------------------------------------------------------------- /src-tauri/src/models/history/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod history; 2 | 3 | pub use history::{ 4 | CommandHistoryEntry, ExportHistoryRequest, GetTerminalHistoryRequest, SearchHistoryRequest, 5 | SearchHistoryResponse, 6 | }; 7 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import type { DefineComponent } from "vue"; 5 | const component: DefineComponent<{}, {}, any>; 6 | export default component; 7 | } 8 | -------------------------------------------------------------------------------- /src-tauri/src/models/sftp/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod file_entry; 3 | pub mod requests; 4 | pub mod search; 5 | pub mod sync; 6 | pub mod transfer; 7 | 8 | // Re-export FileType which is commonly used 9 | pub use file_entry::FileType; 10 | -------------------------------------------------------------------------------- /src-tauri/src/database/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod encryption; 3 | pub mod error; 4 | pub mod providers; 5 | pub mod service; 6 | pub mod traits; 7 | pub mod traits_sync; 8 | 9 | pub use service::{DatabaseService, DatabaseServiceConfig}; 10 | -------------------------------------------------------------------------------- /src-tauri/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod base; 3 | pub mod buffer; 4 | pub mod history; 5 | pub mod recording; 6 | pub mod saved_command; 7 | pub mod sftp; 8 | pub mod ssh; 9 | pub mod sync; 10 | pub mod system; 11 | pub mod terminal; 12 | -------------------------------------------------------------------------------- /src-tauri/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_events; 2 | pub mod buffer; 3 | pub mod dashboard; 4 | pub mod database; 5 | pub mod history; 6 | pub mod recording; 7 | pub mod sftp; 8 | pub mod system; 9 | pub mod terminal; 10 | pub mod terminal_profile; 11 | -------------------------------------------------------------------------------- /src/services/settings.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./api"; 2 | 3 | /** 4 | * Get system fonts 5 | * @returns Array of system font names 6 | */ 7 | export async function getSystemFonts(): Promise { 8 | return await api.callRaw("get_system_fonts"); 9 | } 10 | -------------------------------------------------------------------------------- /src-tauri/capabilities/desktop.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "desktop-capability", 3 | "platforms": [ 4 | "macOS", 5 | "windows", 6 | "linux" 7 | ], 8 | "windows": [ 9 | "main" 10 | ], 11 | "permissions": [ 12 | "updater:default" 13 | ] 14 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src-tauri/src/database/providers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mongodb; 2 | pub mod mysql; 3 | pub mod postgres; 4 | pub mod sqlite; 5 | 6 | pub use mongodb::MongoDBProvider; 7 | pub use mysql::MySQLProvider; 8 | pub use postgres::PostgreSQLProvider; 9 | pub use sqlite::SQLiteProvider; 10 | -------------------------------------------------------------------------------- /src-tauri/src/models/saved_command/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | pub mod group; 3 | 4 | pub use command::{CreateSavedCommandRequest, SavedCommand, UpdateSavedCommandRequest}; 5 | pub use group::{ 6 | CreateSavedCommandGroupRequest, SavedCommandGroup, UpdateSavedCommandGroupRequest, 7 | }; 8 | -------------------------------------------------------------------------------- /src-tauri/src/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod buffer_manager; 3 | pub mod history; 4 | pub mod recording; 5 | pub mod saved_command; 6 | pub mod sftp; 7 | pub mod ssh; 8 | pub mod ssh_config_parser; 9 | pub mod sync; 10 | pub mod terminal; 11 | pub mod tunnel; 12 | pub mod updater; 13 | -------------------------------------------------------------------------------- /src-tauri/src/models/sftp/search.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Result of a file search 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct SearchResult { 7 | pub file_path: String, 8 | pub line_number: u64, 9 | pub content: String, 10 | } 11 | -------------------------------------------------------------------------------- /src/components/terminal-profiles/TerminalProfileManager.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{js,ts,vue}] 11 | indent_size = 2 12 | 13 | [*.{json,md}] 14 | indent_size = 2 15 | max_line_length = 80 16 | 17 | [*.{rs}] 18 | indent_size = 4 19 | max_line_length = 100 20 | -------------------------------------------------------------------------------- /src-tauri/capabilities/clipboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/acl-manifests.json", 3 | "identifier": "clipboard", 4 | "description": "Capability for clipboard access", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "clipboard-manager:allow-read-text", 10 | "clipboard-manager:allow-write-text" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src-tauri/src/database/encryption/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod aes; 2 | pub mod device_keys; 3 | pub mod external_db; 4 | pub mod keychain; 5 | pub mod master_password; 6 | 7 | pub use aes::AESEncryption; 8 | pub use device_keys::DeviceKeyManager; 9 | pub use external_db::ExternalDbEncryptor; 10 | pub use keychain::KeychainManager; 11 | pub use master_password::MasterPasswordManager; 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Kerminal 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | target 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | # Android keystore 29 | .android/*.keystore 30 | -------------------------------------------------------------------------------- /src-tauri/src/models/terminal/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod profile; 2 | pub mod requests; 3 | pub mod terminal; 4 | 5 | pub use requests::*; 6 | 7 | pub use terminal::{ 8 | CreateTerminalRequest, CreateTerminalResponse, LocalConfig, ResizeTerminalRequest, 9 | TerminalConfig, TerminalData, TerminalExited, TerminalInfo, TerminalLatency, TerminalState, 10 | TerminalTitleChanged, TerminalType, WriteBatchTerminalRequest, WriteTerminalRequest, 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/recording/RecordingsModal.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src-tauri/src/models/sync/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod conflict; 2 | pub mod external_db; 3 | pub mod log; 4 | pub mod progress; 5 | pub mod settings; 6 | pub mod stats; 7 | 8 | pub use conflict::ConflictResolutionStrategy; 9 | pub use external_db::{DatabaseType, ExternalDatabaseConfig}; 10 | pub use log::{SyncDirection, SyncLog}; 11 | pub use progress::SyncProgressEvent; 12 | pub use settings::{SyncSettings, UpdateSyncSettingsRequest}; 13 | pub use stats::SyncStats; 14 | -------------------------------------------------------------------------------- /src/core/performance/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Performance optimization exports 3 | */ 4 | 5 | export { InputBatcher } from "./InputBatcher"; 6 | export { TerminalCache, terminalCache } from "./TerminalCache"; 7 | export type { CacheStats } from "./TerminalCache"; 8 | export { IncrementalBufferLoader } from "./IncrementalBufferLoader"; 9 | export type { 10 | SimpleTerminal, 11 | TerminalBufferChunk, 12 | LoadProgressCallback, 13 | LoadOptions, 14 | } from "./IncrementalBufferLoader"; 15 | -------------------------------------------------------------------------------- /src/types/splitpanes.d.ts: -------------------------------------------------------------------------------- 1 | declare module "splitpanes" { 2 | import { DefineComponent } from "vue"; 3 | 4 | export const Splitpanes: DefineComponent<{ 5 | horizontal?: boolean; 6 | pushOtherPanes?: boolean; 7 | dblClickSplitter?: boolean; 8 | resizerStyle?: object; 9 | class?: string; 10 | }>; 11 | 12 | export const Pane: DefineComponent<{ 13 | size?: number; 14 | minSize?: number; 15 | maxSize?: number; 16 | class?: string; 17 | }>; 18 | } 19 | -------------------------------------------------------------------------------- /src-tauri/src/models/auth/requests.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Request for verifying master password 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | pub struct VerifyMasterPasswordRequest { 6 | pub password: String, 7 | } 8 | 9 | /// Request for changing master password 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct ChangeMasterPasswordRequest { 13 | pub old_password: String, 14 | pub new_password: String, 15 | } 16 | -------------------------------------------------------------------------------- /src/types/form.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from "vue"; 2 | 3 | /** 4 | * Props interface for Form component 5 | */ 6 | export interface FormContext { 7 | register: (field: FormField) => void; 8 | unregister: (id: string) => void; 9 | getFieldValue: (id: string) => unknown; 10 | getAllFieldValues: () => { [key: string]: unknown }; 11 | } 12 | 13 | /** 14 | * Interface representing a form field 15 | */ 16 | export interface FormField { 17 | id: string; 18 | value: Ref; 19 | validate: () => string; 20 | } 21 | -------------------------------------------------------------------------------- /src-tauri/src/models/sync/stats.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Sync statistics for monitoring 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct SyncStats { 8 | pub total_records: u32, 9 | pub synced_records: u32, 10 | pub pending_records: u32, 11 | pub failed_records: u32, 12 | pub conflicts: u32, 13 | pub last_sync: Option>, 14 | pub sync_enabled: bool, 15 | pub databases: Vec, 16 | } 17 | -------------------------------------------------------------------------------- /src-tauri/src/commands/database/mod.rs: -------------------------------------------------------------------------------- 1 | /// Common utilities and error handling for database commands 2 | pub mod common; 3 | 4 | /// Master password management commands 5 | pub mod auth; 6 | 7 | /// SSH profile and group management commands 8 | pub mod ssh; 9 | 10 | /// SSH tunnel management commands 11 | pub mod tunnel; 12 | 13 | /// Saved command management commands 14 | pub mod saved_command; 15 | 16 | /// External database management commands 17 | pub mod external_db; 18 | 19 | /// Sync operations and conflict management commands 20 | pub mod sync; 21 | 22 | /// Backup and Restore commands 23 | pub mod backup; 24 | -------------------------------------------------------------------------------- /src-tauri/src/models/ssh/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config_host; 2 | pub mod group; 3 | pub mod key; 4 | pub mod profile; 5 | pub mod tunnel; 6 | 7 | pub use config_host::SSHConfigHost; 8 | pub use group::{CreateSSHGroupRequest, DeleteGroupAction, SSHGroup, UpdateSSHGroupRequest}; 9 | pub use key::{CreateSSHKeyRequest, SSHKey, UpdateSSHKeyRequest}; 10 | pub use profile::{ 11 | AuthData, CreateSSHProfileRequest, SSHProfile, TestSSHConnectionRequest, 12 | UpdateSSHProfileRequest, 13 | }; 14 | pub use tunnel::{ 15 | CreateSSHTunnelRequest, SSHTunnel, TunnelStatus, TunnelType, TunnelWithStatus, 16 | UpdateSSHTunnelRequest, 17 | }; 18 | -------------------------------------------------------------------------------- /src/types/history.ts: -------------------------------------------------------------------------------- 1 | export interface CommandHistoryEntry { 2 | command: string; 3 | timestamp?: string; 4 | index: number; 5 | } 6 | 7 | export interface GetTerminalHistoryRequest { 8 | terminalId: string; 9 | limit?: number; 10 | } 11 | 12 | export interface SearchHistoryRequest { 13 | terminalId: string; 14 | query: string; 15 | limit?: number; 16 | } 17 | 18 | export interface SearchHistoryResponse { 19 | entries: CommandHistoryEntry[]; 20 | totalCount: number; 21 | } 22 | 23 | export interface ExportHistoryRequest { 24 | terminalId: string; 25 | format: "json" | "txt"; 26 | filePath: string; 27 | query?: string; 28 | } 29 | -------------------------------------------------------------------------------- /src-tauri/src/database/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Master password configuration 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | pub struct MasterPasswordConfig { 6 | pub auto_unlock: bool, 7 | pub session_timeout_minutes: Option, 8 | pub require_on_startup: bool, 9 | pub use_keychain: bool, 10 | } 11 | 12 | impl Default for MasterPasswordConfig { 13 | fn default() -> Self { 14 | Self { 15 | auto_unlock: false, 16 | session_timeout_minutes: Some(15), 17 | require_on_startup: true, 18 | use_keychain: true, 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src-tauri/src/models/sync/conflict.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | pub use super::external_db::ConflictResolutionStrategy; 5 | 6 | /// Conflict resolution record stored in database 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct ConflictResolution { 10 | pub id: String, 11 | pub entity_type: String, 12 | pub entity_id: String, 13 | pub local_data: serde_json::Value, 14 | pub remote_data: serde_json::Value, 15 | pub resolution_strategy: Option, 16 | pub resolved_at: Option>, 17 | pub created_at: DateTime, 18 | } 19 | -------------------------------------------------------------------------------- /src-tauri/src/commands/database/common.rs: -------------------------------------------------------------------------------- 1 | use crate::database::error::DatabaseError; 2 | 3 | /// Convert DatabaseError to String for Tauri compatibility 4 | impl From for String { 5 | fn from(error: DatabaseError) -> Self { 6 | let app_error: crate::error::AppError = error.into(); 7 | app_error.to_string() 8 | } 9 | } 10 | 11 | /// Unified error conversion macro for database operations 12 | macro_rules! app_result { 13 | ($expr:expr) => { 14 | $expr.map_err(|e: crate::database::error::DatabaseError| -> String { 15 | let app_error: crate::error::AppError = e.into(); 16 | app_error.to_string() 17 | }) 18 | }; 19 | } 20 | 21 | pub(crate) use app_result; 22 | -------------------------------------------------------------------------------- /src/types/terminalProfile.ts: -------------------------------------------------------------------------------- 1 | export interface TerminalProfile { 2 | id: string; 3 | name: string; 4 | shell: string; 5 | workingDir?: string; 6 | env?: Record; 7 | icon?: string; 8 | color?: string; 9 | command?: string; 10 | } 11 | 12 | export interface CreateTerminalProfileRequest { 13 | name: string; 14 | shell: string; 15 | workingDir?: string; 16 | env?: Record; 17 | icon?: string; 18 | color?: string; 19 | command?: string; 20 | } 21 | 22 | export interface UpdateTerminalProfileRequest { 23 | name?: string; 24 | shell?: string; 25 | workingDir?: string; 26 | env?: Record; 27 | icon?: string; 28 | color?: string; 29 | command?: string; 30 | } 31 | -------------------------------------------------------------------------------- /src/stores/viewState.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { ref } from "vue"; 3 | 4 | /** 5 | * UI State Store 6 | * Manages the state of current active views. 7 | */ 8 | export const useViewStateStore = defineStore("viewState", () => { 9 | const isTopBarActive = ref(false); 10 | 11 | const activeView = ref<"dashboard" | "workspace" | "sftp">("workspace"); 12 | 13 | function setActiveView(view: "dashboard" | "workspace" | "sftp") { 14 | activeView.value = view; 15 | } 16 | 17 | function toggleTopBar(status?: boolean) { 18 | isTopBarActive.value = status ?? !isTopBarActive.value; 19 | } 20 | 21 | return { 22 | isTopBarActive, 23 | activeView, 24 | setActiveView, 25 | toggleTopBar, 26 | }; 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/CommandPaletteManager.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2021", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "jsx": "preserve", 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "@/*": ["src/*"] 21 | } 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | import pkg from "./package.json"; 5 | 6 | // @ts-expect-error process is a nodejs global 7 | const host = process.env.TAURI_DEV_HOST; 8 | 9 | export default defineConfig(async () => ({ 10 | plugins: [vue(), tailwindcss()], 11 | clearScreen: false, 12 | server: { 13 | port: 1420, 14 | strictPort: true, 15 | host: host || false, 16 | hmr: host 17 | ? { 18 | protocol: "ws", 19 | host, 20 | port: 1421, 21 | } 22 | : undefined, 23 | watch: { 24 | ignored: ["**/src-tauri/**"], 25 | }, 26 | }, 27 | define: { 28 | __APP_VERSION__: JSON.stringify(pkg.version), 29 | }, 30 | })); 31 | -------------------------------------------------------------------------------- /src/components/auth/MasterPasswordManager.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 27 | -------------------------------------------------------------------------------- /src/types/recording.ts: -------------------------------------------------------------------------------- 1 | export interface SessionRecording { 2 | id: string; 3 | terminalId: string; 4 | sessionName: string; 5 | terminalType: "Local" | "SSH"; 6 | startedAt: string; 7 | endedAt?: string; 8 | durationMs?: number; 9 | filePath: string; 10 | fileSize: number; 11 | width: number; 12 | height: number; 13 | metadata?: string; 14 | createdAt: string; 15 | } 16 | 17 | export interface RecordingMetadata { 18 | tags: string[]; 19 | description?: string; 20 | sshHost?: string; 21 | shell?: string; 22 | } 23 | 24 | export interface AsciicastHeader { 25 | version: number; 26 | width: number; 27 | height: number; 28 | timestamp?: number; 29 | title?: string; 30 | env?: Record; 31 | } 32 | 33 | export interface AsciicastEvent { 34 | time: number; 35 | eventType: string; 36 | data: string; 37 | } 38 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:default", 10 | "opener:default", 11 | { 12 | "identifier": "opener:allow-open-path", 13 | "allow": [ 14 | { 15 | "path": "**/*" 16 | } 17 | ] 18 | }, 19 | "store:default", 20 | "dialog:default", 21 | "fs:default", 22 | "fs:write-all", 23 | "fs:read-all", 24 | "fs:read-dirs", 25 | "fs:allow-rename", 26 | "fs:allow-mkdir", 27 | "fs:allow-exists", 28 | "fs:allow-watch", 29 | "fs:allow-stat", 30 | { 31 | "identifier": "fs:scope", 32 | "allow": [ 33 | { 34 | "path": "**/*" 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/types/asciinema-player.d.ts: -------------------------------------------------------------------------------- 1 | declare module "asciinema-player" { 2 | export interface PlayerOptions { 3 | cols?: number; 4 | rows?: number; 5 | autoPlay?: boolean; 6 | preload?: boolean; 7 | loop?: boolean | number; 8 | startAt?: number | string; 9 | speed?: number; 10 | idleTimeLimit?: number; 11 | theme?: string; 12 | poster?: string; 13 | fit?: string; 14 | fontSize?: string; 15 | terminalFontSize?: string; 16 | terminalFontFamily?: string; 17 | terminalLineHeight?: number; 18 | } 19 | 20 | export interface Player { 21 | dispose(): void; 22 | play(): void; 23 | pause(): void; 24 | seek(time: number): void; 25 | getCurrentTime(): number; 26 | getDuration(): number; 27 | } 28 | 29 | export function create( 30 | src: string | object, 31 | element: HTMLElement, 32 | options?: PlayerOptions, 33 | ): Player; 34 | } 35 | -------------------------------------------------------------------------------- /src/types/overlay.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from "vue"; 2 | 3 | export interface OverlayConfig { 4 | id: string; 5 | type: "drawer" | "modal"; 6 | component?: Component | string; 7 | props?: Record; 8 | parentId?: string | null; 9 | title?: string; 10 | icon?: Component; 11 | metadata?: Record; 12 | onBeforeOpen?: () => void | Promise; 13 | onOpened?: () => void; 14 | onBeforeClose?: () => boolean | Promise; 15 | onClosed?: () => void; 16 | onError?: (error: Error) => void; 17 | } 18 | 19 | export interface OverlayState { 20 | config: OverlayConfig; 21 | visible: boolean; 22 | transitioning: boolean; 23 | zIndex: number; 24 | createdAt: number; 25 | lastAccessedAt?: number; 26 | } 27 | 28 | export interface OverlayManagerState { 29 | overlays: Map; 30 | activeOverlayId: string | null; 31 | history: string[]; 32 | baseZIndex: number; 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/terminalTheme.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TERMINAL_THEMES, 3 | getAvailableThemeNames, 4 | isBuiltInTheme as isBuiltInThemeConfig, 5 | type TerminalTheme, 6 | } from "../config/terminalThemes"; 7 | 8 | // Re-export type for backward compatibility 9 | export type { TerminalTheme } from "../config/terminalThemes"; 10 | 11 | export function getTerminalTheme( 12 | themeName: keyof typeof TERMINAL_THEMES = "Default", 13 | customTheme?: TerminalTheme, 14 | ): TerminalTheme { 15 | // If custom theme is provided, use it 16 | if (customTheme) { 17 | return customTheme; 18 | } 19 | 20 | // Otherwise, look up in built-in themes 21 | return TERMINAL_THEMES[themeName] || TERMINAL_THEMES["Default"]; 22 | } 23 | 24 | export function getAvailableThemes(): (keyof typeof TERMINAL_THEMES)[] { 25 | return getAvailableThemeNames(); 26 | } 27 | 28 | export function isBuiltInTheme(themeName: string): boolean { 29 | return isBuiltInThemeConfig(themeName); 30 | } 31 | -------------------------------------------------------------------------------- /src-tauri/src/models/recording/recording.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct SessionRecording { 7 | pub id: String, 8 | pub terminal_id: String, 9 | pub session_name: String, 10 | pub terminal_type: String, // "Local" | "SSH" 11 | pub started_at: DateTime, 12 | pub ended_at: Option>, 13 | pub duration_ms: Option, 14 | pub file_path: String, 15 | pub file_size: i64, 16 | pub width: u16, 17 | pub height: u16, 18 | pub metadata: Option, // JSON 19 | pub created_at: DateTime, 20 | } 21 | 22 | #[derive(Debug, Clone, Serialize, Deserialize)] 23 | pub struct AsciicastHeader { 24 | pub version: u8, 25 | pub width: u16, 26 | pub height: u16, 27 | pub timestamp: Option, 28 | pub title: Option, 29 | pub env: Option, 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ui/SkeletonText.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 36 | -------------------------------------------------------------------------------- /src/components/tunnels/TunnelManager.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 44 | -------------------------------------------------------------------------------- /src/services/history.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./api"; 2 | import type { 3 | CommandHistoryEntry, 4 | GetTerminalHistoryRequest, 5 | SearchHistoryRequest, 6 | SearchHistoryResponse, 7 | ExportHistoryRequest, 8 | } from "../types/history"; 9 | 10 | /** 11 | * History service for frontend 12 | */ 13 | export const historyService = { 14 | /** 15 | * Get history for a terminal 16 | */ 17 | async getHistory( 18 | request: GetTerminalHistoryRequest, 19 | ): Promise { 20 | return await api.call( 21 | "get_terminal_history", 22 | request, 23 | ); 24 | }, 25 | 26 | /** 27 | * Search history for a terminal 28 | */ 29 | async searchHistory( 30 | request: SearchHistoryRequest, 31 | ): Promise { 32 | return await api.call("search_history", request); 33 | }, 34 | 35 | /** 36 | * Export history to file 37 | */ 38 | async exportHistory(request: ExportHistoryRequest): Promise { 39 | return await api.call("export_history", request); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Bùi Thanh Xuân (klpod221) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/recording/RecordingsManager.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 44 | -------------------------------------------------------------------------------- /src-tauri/src/commands/history.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppError; 2 | use crate::models::history::{ 3 | CommandHistoryEntry, ExportHistoryRequest, GetTerminalHistoryRequest, SearchHistoryRequest, 4 | SearchHistoryResponse, 5 | }; 6 | use crate::state::AppState; 7 | use tauri::State; 8 | 9 | /// Get history for a terminal 10 | #[tauri::command] 11 | pub async fn get_terminal_history( 12 | request: GetTerminalHistoryRequest, 13 | app_state: State<'_, AppState>, 14 | ) -> Result, AppError> { 15 | app_state.history_manager.get_history(request).await 16 | } 17 | 18 | /// Search history for a terminal 19 | #[tauri::command] 20 | pub async fn search_history( 21 | request: SearchHistoryRequest, 22 | app_state: State<'_, AppState>, 23 | ) -> Result { 24 | app_state.history_manager.search_history(request).await 25 | } 26 | 27 | /// Export history to file 28 | #[tauri::command] 29 | pub async fn export_history( 30 | request: ExportHistoryRequest, 31 | app_state: State<'_, AppState>, 32 | ) -> Result { 33 | app_state.history_manager.export_history(request).await 34 | } 35 | -------------------------------------------------------------------------------- /src/types/system.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * System information types 3 | */ 4 | 5 | /** 6 | * Interface for CPU information 7 | */ 8 | export interface CPUInfo { 9 | model: string; 10 | speed: number; 11 | times: { 12 | user: number; 13 | nice: number; 14 | sys: number; 15 | idle: number; 16 | irq: number; 17 | }; 18 | } 19 | 20 | /** 21 | * Interface for system information 22 | */ 23 | export interface SystemInfo { 24 | platform: string; 25 | release: string; 26 | arch: string; 27 | hostname: string; 28 | uptime: number; 29 | totalMemory: number; 30 | freeMemory: number; 31 | loadAverage: number[]; 32 | cpus: CPUInfo[]; 33 | osRelease?: string; 34 | cpuInfo?: string; 35 | memInfo?: string; 36 | gpuInfo?: string; 37 | resolution?: string; 38 | } 39 | 40 | /** 41 | * Interface for network interface information 42 | */ 43 | export interface NetworkInterface { 44 | name: string; 45 | address: string; 46 | netmask: string; 47 | mac: string; 48 | isConnected?: boolean; 49 | } 50 | 51 | /** 52 | * Interface for network status 53 | */ 54 | export interface NetworkStatus { 55 | isConnected: boolean; 56 | primaryInterface: NetworkInterface | null; 57 | interfaces: NetworkInterface[]; 58 | } 59 | -------------------------------------------------------------------------------- /src/services/recording.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./api"; 2 | import type { SessionRecording } from "../types/recording"; 3 | 4 | export async function startRecording( 5 | terminalId: string, 6 | sessionName?: string, 7 | width?: number, 8 | height?: number, 9 | ): Promise { 10 | return await api.call("start_recording", { 11 | terminalId, 12 | sessionName, 13 | width, 14 | height, 15 | }); 16 | } 17 | 18 | export async function stopRecording( 19 | terminalId: string, 20 | ): Promise { 21 | return await api.call("stop_recording", { terminalId }); 22 | } 23 | 24 | export async function listRecordings(): Promise { 25 | return await api.call("list_recordings"); 26 | } 27 | 28 | export async function deleteRecording(recordingId: string): Promise { 29 | return await api.call("delete_recording", { recordingId }); 30 | } 31 | 32 | export async function exportRecording( 33 | recordingId: string, 34 | exportPath: string, 35 | ): Promise { 36 | return await api.call("export_recording", { recordingId, exportPath }); 37 | } 38 | 39 | export async function readCastFile(filePath: string): Promise { 40 | return await api.call("read_cast_file", { filePath }); 41 | } 42 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import { createPinia } from "pinia"; 3 | import "./assets/css/main.css"; 4 | import App from "./App.vue"; 5 | 6 | // Configure Monaco Editor Workers 7 | import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; 8 | import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; 9 | import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; 10 | import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; 11 | import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; 12 | 13 | globalThis.MonacoEnvironment = { 14 | getWorker(_worker: unknown, label: string) { 15 | if (label === "json") { 16 | return new jsonWorker(); 17 | } 18 | if (label === "css" || label === "scss" || label === "less") { 19 | return new cssWorker(); 20 | } 21 | if (label === "html" || label === "handlebars" || label === "razor") { 22 | return new htmlWorker(); 23 | } 24 | if (label === "typescript" || label === "javascript") { 25 | return new tsWorker(); 26 | } 27 | return new editorWorker(); 28 | }, 29 | }; 30 | 31 | const pinia = createPinia(); 32 | const app = createApp(App); 33 | 34 | app.use(pinia); 35 | app.mount("#app"); 36 | -------------------------------------------------------------------------------- /src-tauri/src/services/ssh/connection_pool.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | use std::time::{Duration, Instant}; 4 | use tokio::sync::RwLock; 5 | 6 | struct PooledConnection { 7 | last_used: Instant, 8 | active: bool, 9 | } 10 | 11 | pub struct SSHConnectionPool { 12 | connections: Arc>>, 13 | max_idle_time: Duration, 14 | } 15 | 16 | impl SSHConnectionPool { 17 | pub fn new(max_idle_minutes: u64) -> Self { 18 | Self { 19 | connections: Arc::new(RwLock::new(HashMap::new())), 20 | max_idle_time: Duration::from_secs(max_idle_minutes * 60), 21 | } 22 | } 23 | 24 | pub async fn clear(&self) { 25 | let mut pool = self.connections.write().await; 26 | pool.clear(); 27 | } 28 | 29 | pub async fn cleanup_idle(&self) { 30 | let mut pool = self.connections.write().await; 31 | pool.retain(|_, conn| conn.active && conn.last_used.elapsed() < self.max_idle_time); 32 | } 33 | 34 | pub async fn pool_size(&self) -> usize { 35 | let pool = self.connections.read().await; 36 | pool.len() 37 | } 38 | } 39 | 40 | impl Default for SSHConnectionPool { 41 | fn default() -> Self { 42 | Self::new(30) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/tunnels/TunnelStatusIndicator.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 50 | -------------------------------------------------------------------------------- /src/components/saved-commands/SavedCommandManager.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | -------------------------------------------------------------------------------- /src-tauri/src/database/traits_sync.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use chrono::{DateTime, Utc}; 3 | use serde_json::Value; 4 | 5 | use crate::database::error::DatabaseResult; 6 | 7 | /// Simplified trait for sync target databases 8 | /// These databases only serve as sync endpoints - no business logic 9 | #[async_trait] 10 | pub trait SyncTarget: Send + Sync { 11 | /// Connect to the database 12 | async fn connect(&mut self) -> DatabaseResult<()>; 13 | 14 | /// Test the database connection 15 | async fn test_connection(&self) -> DatabaseResult<()>; 16 | 17 | /// Push records to remote database 18 | /// Records are JSON-serialized entities from local SQLite 19 | async fn push_records(&self, table: &str, records: Vec) -> DatabaseResult; 20 | 21 | /// Pull records from remote database modified since timestamp 22 | /// Returns JSON-serialized entities 23 | async fn pull_records( 24 | &self, 25 | table: &str, 26 | since: Option>, 27 | ) -> DatabaseResult>; 28 | 29 | /// Get record versions for conflict detection 30 | /// Get version information for conflict detection 31 | async fn get_record_versions( 32 | &self, 33 | table: &str, 34 | ids: Vec, 35 | ) -> DatabaseResult>; 36 | } 37 | -------------------------------------------------------------------------------- /src/types/tunnel.ts: -------------------------------------------------------------------------------- 1 | export type TunnelType = "Local" | "Remote" | "Dynamic"; 2 | export type TunnelStatus = "stopped" | "starting" | "running" | "error"; 3 | 4 | export interface BaseModel { 5 | id: string; 6 | createdAt: string; 7 | updatedAt: string; 8 | deviceId: string; 9 | version: number; 10 | syncStatus: "synced" | "pending" | "conflict"; 11 | } 12 | 13 | export interface SSHTunnel extends BaseModel { 14 | name: string; 15 | description?: string; 16 | profileId: string; 17 | tunnelType: TunnelType; 18 | localHost: string; 19 | localPort: number; 20 | remoteHost?: string; 21 | remotePort?: number; 22 | autoStart: boolean; 23 | } 24 | 25 | export interface TunnelWithStatus extends SSHTunnel { 26 | status: TunnelStatus; 27 | errorMessage?: string; 28 | } 29 | 30 | export interface CreateSSHTunnelRequest { 31 | name: string; 32 | description?: string; 33 | profileId: string; 34 | tunnelType: TunnelType; 35 | localHost: string; 36 | localPort: number; 37 | remoteHost?: string; 38 | remotePort?: number; 39 | autoStart?: boolean; 40 | } 41 | 42 | export interface UpdateSSHTunnelRequest { 43 | name?: string; 44 | description?: string; 45 | profileId?: string; 46 | tunnelType?: TunnelType; 47 | localHost?: string; 48 | localPort?: number; 49 | remoteHost?: string; 50 | remotePort?: number; 51 | autoStart?: boolean; 52 | } 53 | -------------------------------------------------------------------------------- /src-tauri/src/models/terminal/profile.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct TerminalProfile { 7 | pub id: String, 8 | pub name: String, 9 | pub shell: String, 10 | pub working_dir: Option, 11 | pub env: Option>, 12 | pub icon: Option, 13 | pub color: Option, 14 | pub command: Option, 15 | pub created_at: i64, 16 | pub updated_at: i64, 17 | } 18 | 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct CreateTerminalProfileRequest { 22 | pub name: String, 23 | pub shell: String, 24 | pub working_dir: Option, 25 | pub env: Option>, 26 | pub icon: Option, 27 | pub color: Option, 28 | pub command: Option, 29 | } 30 | 31 | #[derive(Debug, Clone, Serialize, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct UpdateTerminalProfileRequest { 34 | pub name: Option, 35 | pub shell: Option, 36 | pub working_dir: Option, 37 | pub env: Option>, 38 | pub icon: Option, 39 | pub color: Option, 40 | pub command: Option, 41 | } 42 | -------------------------------------------------------------------------------- /src-tauri/src/models/buffer.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Request for getting terminal buffer 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct GetTerminalBufferRequest { 7 | pub terminal_id: String, 8 | } 9 | 10 | /// Request for getting terminal buffer chunk 11 | #[derive(Debug, Clone, Serialize, Deserialize)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct GetTerminalBufferChunkRequest { 14 | pub terminal_id: String, 15 | pub start_line: usize, 16 | pub chunk_size: usize, 17 | } 18 | 19 | /// Terminal buffer chunk response 20 | #[derive(Debug, Clone, Serialize, Deserialize)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct TerminalBufferChunk { 23 | pub terminal_id: String, 24 | pub start_line: usize, 25 | pub end_line: usize, 26 | pub total_lines: usize, 27 | pub data: String, 28 | pub has_more: bool, 29 | } 30 | 31 | /// Request for checking if terminal has buffer 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct HasTerminalBufferRequest { 35 | pub terminal_id: String, 36 | } 37 | 38 | /// Request for cleaning up terminal buffers 39 | #[derive(Debug, Clone, Serialize, Deserialize)] 40 | #[serde(rename_all = "camelCase")] 41 | pub struct CleanupTerminalBuffersRequest { 42 | pub active_terminal_ids: Vec, 43 | } 44 | -------------------------------------------------------------------------------- /src-tauri/src/models/terminal/requests.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Request for creating a new SSH terminal 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct CreateSshTerminalRequest { 7 | pub profile_id: String, 8 | } 9 | 10 | /// Request for creating a new SSH terminal from SSH config host 11 | #[derive(Debug, Clone, Serialize, Deserialize)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct CreateSshConfigTerminalRequest { 14 | pub host_name: String, 15 | pub title: Option, 16 | pub password: Option, 17 | } 18 | 19 | /// Request for creating a local terminal 20 | #[derive(Debug, Clone, Serialize, Deserialize)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct CreateLocalTerminalRequest { 23 | pub shell: Option, 24 | pub working_dir: Option, 25 | pub title: Option, 26 | pub terminal_profile_id: Option, 27 | pub command: Option, 28 | } 29 | 30 | /// Request for closing a terminal 31 | #[derive(Debug, Clone, Serialize, Deserialize)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct CloseTerminalRequest { 34 | pub terminal_id: String, 35 | } 36 | 37 | /// Request for getting terminal info 38 | #[derive(Debug, Clone, Serialize, Deserialize)] 39 | #[serde(rename_all = "camelCase")] 40 | pub struct GetTerminalInfoRequest { 41 | pub terminal_id: String, 42 | } 43 | -------------------------------------------------------------------------------- /src/components/sync/SyncStatusIndicator.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 30 | 31 | 59 | -------------------------------------------------------------------------------- /src/components/ui/EmptyState.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 62 | -------------------------------------------------------------------------------- /src-tauri/src/models/sftp/file_entry.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Represents a file or directory entry on the remote system 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct FileEntry { 8 | /// File or directory name 9 | pub name: String, 10 | /// Full path 11 | pub path: String, 12 | /// File type 13 | pub file_type: FileType, 14 | /// File size in bytes (None for directories) 15 | pub size: Option, 16 | /// Permissions (Unix-style, e.g., 0o755) 17 | pub permissions: u32, 18 | /// Last modified time 19 | pub modified: DateTime, 20 | /// Last accessed time 21 | pub accessed: Option>, 22 | /// Symlink target (if this is a symlink) 23 | pub symlink_target: Option, 24 | /// Owner user ID 25 | pub uid: Option, 26 | /// Owner group ID 27 | pub gid: Option, 28 | } 29 | 30 | /// File type enumeration 31 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 32 | #[serde(rename_all = "lowercase")] 33 | pub enum FileType { 34 | /// Regular file 35 | File, 36 | /// Directory 37 | Directory, 38 | /// Symbolic link 39 | Symlink, 40 | /// Unknown type 41 | Unknown, 42 | } 43 | 44 | impl FileEntry { 45 | /// Check if this entry is a directory 46 | pub fn is_directory(&self) -> bool { 47 | matches!(self.file_type, FileType::Directory) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/settings/SettingsManager.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 52 | -------------------------------------------------------------------------------- /src-tauri/src/models/saved_command/group.rs: -------------------------------------------------------------------------------- 1 | use crate::models::base::BaseModel; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Saved command group model 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct SavedCommandGroup { 8 | #[serde(flatten)] 9 | pub base: BaseModel, 10 | pub name: String, 11 | pub description: Option, 12 | pub color: Option, 13 | pub icon: Option, 14 | } 15 | 16 | /// Request to create a new saved command group 17 | #[derive(Debug, Clone, Serialize, Deserialize)] 18 | #[serde(rename_all = "camelCase")] 19 | pub struct CreateSavedCommandGroupRequest { 20 | pub name: String, 21 | pub description: Option, 22 | pub color: Option, 23 | pub icon: Option, 24 | } 25 | 26 | /// Request to update an existing saved command group 27 | #[derive(Debug, Clone, Serialize, Deserialize)] 28 | #[serde(rename_all = "camelCase")] 29 | pub struct UpdateSavedCommandGroupRequest { 30 | pub name: Option, 31 | pub description: Option, 32 | pub color: Option, 33 | pub icon: Option, 34 | } 35 | 36 | impl SavedCommandGroup { 37 | /// Create a new saved command group 38 | pub fn new(device_id: String, name: String) -> Self { 39 | Self { 40 | base: BaseModel::new(device_id), 41 | name, 42 | description: None, 43 | color: None, 44 | icon: None, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kerminal", 3 | "version": "2.5.5", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "tauri": "tauri", 10 | "pretty": "prettier --write \"./**/*.{ts,vue,css}\"" 11 | }, 12 | "dependencies": { 13 | "@tauri-apps/api": "^2", 14 | "@tauri-apps/plugin-clipboard-manager": "^2.3.0", 15 | "@tauri-apps/plugin-dialog": "^2.4.2", 16 | "@tauri-apps/plugin-fs": "^2.4.4", 17 | "@tauri-apps/plugin-opener": "^2", 18 | "@tauri-apps/plugin-store": "^2.4.1", 19 | "@tauri-apps/plugin-updater": "^2.9.0", 20 | "@xterm/addon-clipboard": "^0.1.0", 21 | "@xterm/addon-fit": "^0.10.0", 22 | "@xterm/addon-search": "^0.15.0", 23 | "@xterm/addon-unicode11": "^0.8.0", 24 | "@xterm/addon-web-links": "^0.11.0", 25 | "@xterm/addon-webgl": "^0.18.0", 26 | "@xterm/xterm": "^5.5.0", 27 | "asciinema-player": "^3.12.1", 28 | "date-fns": "^4.1.0", 29 | "lucide-vue-next": "^0.544.0", 30 | "monaco-editor": "^0.54.0", 31 | "monaco-editor-vue3": "^1.0.4", 32 | "pinia": "^3.0.3", 33 | "prismjs": "^1.30.0", 34 | "splitpanes": "^4.0.4", 35 | "uuid": "^13.0.0", 36 | "vue": "^3.5.13" 37 | }, 38 | "devDependencies": { 39 | "@tailwindcss/vite": "^4.1.13", 40 | "@tauri-apps/cli": "^2", 41 | "@types/prismjs": "^1.26.5", 42 | "@vitejs/plugin-vue": "^5.2.1", 43 | "prettier": "^3.6.2", 44 | "tailwindcss": "^4.1.13", 45 | "typescript": "~5.6.2", 46 | "vite": "^6.0.3", 47 | "vue-tsc": "^2.0.29" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src-tauri/src/models/system/info.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct SystemInfo { 5 | pub platform: String, 6 | pub release: String, 7 | pub cpu_arch: String, 8 | pub hostname: String, 9 | pub uptime: u64, 10 | pub total_memory: u64, 11 | pub free_memory: u64, 12 | pub load_average: (f64, f64, f64), 13 | pub cpus: Vec, 14 | pub os_version: Option, 15 | pub cpu_info: Option, 16 | pub memory_info: Option, 17 | pub gpu_info: Option, 18 | pub resolution: Option<(u32, u32)>, 19 | pub network_interfaces: Option>, 20 | pub disks_info: Option>, 21 | pub components_info: Option>, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Debug, Clone)] 25 | pub struct CPUInfo { 26 | pub model: String, 27 | pub speed: u64, 28 | pub usage: f32, 29 | } 30 | 31 | #[derive(Serialize, Deserialize, Debug, Clone)] 32 | pub struct DiskInfo { 33 | pub name: String, 34 | pub total_space: u64, 35 | pub available_space: u64, 36 | pub file_system: String, 37 | pub mount_point: String, 38 | } 39 | 40 | #[derive(Serialize, Deserialize, Debug, Clone)] 41 | pub struct ComponentInfo { 42 | pub label: String, 43 | pub temperature: f32, 44 | pub max: f32, 45 | } 46 | 47 | #[derive(Serialize, Deserialize, Debug, Clone)] 48 | pub struct NetworkInterface { 49 | pub name: String, 50 | pub address: String, 51 | pub mac: String, 52 | pub status: String, 53 | } 54 | -------------------------------------------------------------------------------- /src-tauri/src/setup.rs: -------------------------------------------------------------------------------- 1 | use crate::state::AppState; 2 | use tauri::{App, Manager}; 3 | 4 | pub fn init(app: &mut App) -> std::result::Result<(), Box> { 5 | #[cfg(desktop)] 6 | { 7 | let window = app.get_webview_window("main").unwrap(); 8 | window.set_title("Kerminal").unwrap(); 9 | } 10 | 11 | let app_handle = app.handle().clone(); 12 | tauri::async_runtime::spawn(async move { 13 | // Initialize updater service 14 | let updater_service = crate::services::updater::UpdaterService::new(app_handle.clone()); 15 | updater_service.start_update_check_loop(); 16 | 17 | match AppState::new().await { 18 | Ok(app_state) => { 19 | let auth_session_manager = app_state.auth_session_manager.clone(); 20 | let sftp_transfer_manager = app_state.sftp_transfer_manager.clone(); 21 | 22 | app_handle.manage(app_state); 23 | 24 | let auth_manager_clone = auth_session_manager.clone(); 25 | let app_handle_clone = app_handle.clone(); 26 | 27 | tokio::spawn(async move { 28 | let mut manager = auth_manager_clone.lock().await; 29 | manager.set_app_handle(app_handle_clone); 30 | let _ = manager.initialize().await; 31 | }); 32 | 33 | sftp_transfer_manager.start_queue_processor(app_handle.clone()); 34 | } 35 | Err(e) => { 36 | eprintln!("Failed to initialize AppState: {}", e); 37 | } 38 | } 39 | }); 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/components/ui/SkeletonList.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 61 | -------------------------------------------------------------------------------- /src/components/Workspace.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 43 | -------------------------------------------------------------------------------- /src/components/ui/Form.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 69 | -------------------------------------------------------------------------------- /src/components/ui/NavigationTabs.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 57 | -------------------------------------------------------------------------------- /src/composables/useWindowSize.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed, onMounted, onUnmounted } from "vue"; 2 | import { getCurrentWindow } from "@tauri-apps/api/window"; 3 | 4 | export const BREAKPOINTS = { 5 | mobile: 480, 6 | tablet: 768, 7 | desktop: 1024, 8 | } as const; 9 | 10 | export type DeviceType = "mobile" | "tablet" | "desktop"; 11 | 12 | /** 13 | * Composable for tracking window size and device type 14 | * @returns Window size, device type, and breakpoint utilities 15 | */ 16 | export function useWindowSize() { 17 | const width = ref(0); 18 | const height = ref(0); 19 | 20 | const updateSize = async () => { 21 | const window = getCurrentWindow(); 22 | const size = await window.innerSize(); 23 | 24 | width.value = size.width; 25 | height.value = size.height; 26 | }; 27 | 28 | const isMobile = computed(() => width.value < BREAKPOINTS.mobile); 29 | const isTablet = computed( 30 | () => 31 | width.value >= BREAKPOINTS.mobile && width.value < BREAKPOINTS.desktop, 32 | ); 33 | const isDesktop = computed(() => width.value >= BREAKPOINTS.desktop); 34 | 35 | const deviceType = computed(() => { 36 | if (isMobile.value) return "mobile"; 37 | if (isTablet.value) return "tablet"; 38 | return "desktop"; 39 | }); 40 | 41 | const isTouch = computed(() => isMobile.value || isTablet.value); 42 | 43 | onMounted(() => { 44 | updateSize(); 45 | window.addEventListener("resize", updateSize); 46 | }); 47 | 48 | onUnmounted(() => { 49 | window.removeEventListener("resize", updateSize); 50 | }); 51 | 52 | return { 53 | width, 54 | height, 55 | isMobile, 56 | isTablet, 57 | isDesktop, 58 | deviceType, 59 | isTouch, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/components/ui/CommandPreview.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | 33 | 71 | -------------------------------------------------------------------------------- /src/services/tunnel.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./api"; 2 | import type { 3 | SSHTunnel, 4 | TunnelWithStatus, 5 | CreateSSHTunnelRequest, 6 | UpdateSSHTunnelRequest, 7 | TunnelStatus, 8 | } from "../types/tunnel"; 9 | 10 | /** 11 | * SSH Tunnel service for frontend 12 | */ 13 | export const tunnelService = { 14 | /** 15 | * Create new SSH tunnel 16 | */ 17 | async createTunnel(request: CreateSSHTunnelRequest): Promise { 18 | return await api.call("create_tunnel", request); 19 | }, 20 | 21 | /** 22 | * Get all SSH tunnels with status 23 | */ 24 | async getTunnels(): Promise { 25 | return await api.callRaw("get_tunnels"); 26 | }, 27 | 28 | /** 29 | * Get SSH tunnel by ID with status 30 | */ 31 | async getTunnel(id: string): Promise { 32 | return await api.callRaw("get_tunnel", id); 33 | }, 34 | 35 | /** 36 | * Update SSH tunnel 37 | */ 38 | async updateTunnel( 39 | id: string, 40 | request: UpdateSSHTunnelRequest, 41 | ): Promise { 42 | return await api.callRaw("update_tunnel", id, request); 43 | }, 44 | 45 | /** 46 | * Delete SSH tunnel 47 | */ 48 | async deleteTunnel(id: string): Promise { 49 | return await api.callRaw("delete_tunnel", id); 50 | }, 51 | 52 | /** 53 | * Start SSH tunnel 54 | */ 55 | async startTunnel(id: string): Promise { 56 | return await api.callRaw("start_tunnel", id); 57 | }, 58 | 59 | /** 60 | * Stop SSH tunnel 61 | */ 62 | async stopTunnel(id: string): Promise { 63 | return await api.callRaw("stop_tunnel", id); 64 | }, 65 | 66 | /** 67 | * Get tunnel status 68 | */ 69 | async getTunnelStatus(id: string): Promise { 70 | return await api.callRaw("get_tunnel_status", id); 71 | }, 72 | }; 73 | -------------------------------------------------------------------------------- /src/types/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Master password setup configuration 3 | */ 4 | export interface MasterPasswordSetup { 5 | password: string; 6 | confirmPassword: string; 7 | deviceName: string; 8 | autoUnlock: boolean; 9 | useKeychain: boolean; 10 | autoLockTimeout: number; // in minutes (0 = never) 11 | } 12 | 13 | /** 14 | * Master password verification request 15 | */ 16 | export interface MasterPasswordVerification { 17 | password: string; 18 | } 19 | 20 | /** 21 | * Master password status information 22 | */ 23 | export interface MasterPasswordStatus { 24 | isSetup: boolean; 25 | isUnlocked: boolean; 26 | autoUnlockEnabled: boolean; 27 | keychainAvailable: boolean; 28 | sessionActive: boolean; 29 | sessionExpiresAt?: string; 30 | loadedDeviceCount: number; 31 | } 32 | 33 | /** 34 | * Master password change request 35 | */ 36 | export interface MasterPasswordChange { 37 | oldPassword: string; 38 | newPassword: string; 39 | confirmNewPassword: string; 40 | } 41 | 42 | /** 43 | * Security settings for master password 44 | */ 45 | export interface SecuritySettings { 46 | autoLockTimeout: number; // in minutes (0 = never) 47 | } 48 | 49 | /** 50 | * Master password configuration 51 | */ 52 | export interface MasterPasswordConfig { 53 | autoUnlock: boolean; 54 | autoLockTimeout: number; // in minutes (0 = never) 55 | } 56 | 57 | /** 58 | * Master password configuration update request 59 | */ 60 | export interface MasterPasswordConfigUpdate extends MasterPasswordConfig { 61 | password?: string; // Required when enabling auto-unlock 62 | } 63 | 64 | /** 65 | * Device information for master password 66 | */ 67 | export interface CurrentDevice { 68 | deviceId: string; 69 | deviceName: string; 70 | deviceType: string; 71 | osName: string; 72 | osVersion: string; 73 | createdAt: string; 74 | } 75 | -------------------------------------------------------------------------------- /src/components/ui/Badge.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 69 | -------------------------------------------------------------------------------- /src-tauri/src/models/sync/log.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Sync log entry 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct SyncLog { 8 | pub id: String, 9 | pub database_id: String, 10 | pub device_id: String, 11 | pub direction: SyncDirection, 12 | pub status: SyncStatus, 13 | pub started_at: DateTime, 14 | pub completed_at: Option>, 15 | pub records_synced: i32, 16 | pub conflicts_resolved: i32, 17 | pub manual_conflicts: i32, 18 | pub error_message: Option, 19 | } 20 | 21 | /// Sync direction 22 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 23 | #[serde(rename_all = "camelCase")] 24 | pub enum SyncDirection { 25 | Push, 26 | Pull, 27 | Bidirectional, 28 | } 29 | 30 | impl std::fmt::Display for SyncDirection { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | match self { 33 | SyncDirection::Push => write!(f, "Push"), 34 | SyncDirection::Pull => write!(f, "Pull"), 35 | SyncDirection::Bidirectional => write!(f, "Bidirectional"), 36 | } 37 | } 38 | } 39 | 40 | impl std::str::FromStr for SyncDirection { 41 | type Err = String; 42 | 43 | fn from_str(s: &str) -> Result { 44 | match s { 45 | "Push" => Ok(SyncDirection::Push), 46 | "Pull" => Ok(SyncDirection::Pull), 47 | "Bidirectional" | "Both" => Ok(SyncDirection::Bidirectional), 48 | _ => Err(format!("Unknown sync direction: {}", s)), 49 | } 50 | } 51 | } 52 | 53 | /// Sync status 54 | #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] 55 | #[serde(rename_all = "camelCase")] 56 | pub enum SyncStatus { 57 | InProgress, 58 | Completed, 59 | Failed, 60 | Cancelled, 61 | } 62 | -------------------------------------------------------------------------------- /src-tauri/src/models/saved_command/command.rs: -------------------------------------------------------------------------------- 1 | use crate::models::base::BaseModel; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// Saved command model 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct SavedCommand { 8 | #[serde(flatten)] 9 | pub base: BaseModel, 10 | pub name: String, 11 | pub description: Option, 12 | pub command: String, 13 | pub group_id: Option, 14 | pub tags: Option, // JSON array as string 15 | pub is_favorite: bool, 16 | pub usage_count: u32, 17 | pub last_used_at: Option, 18 | } 19 | 20 | /// Request to create a new saved command 21 | #[derive(Debug, Clone, Serialize, Deserialize)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct CreateSavedCommandRequest { 24 | pub name: String, 25 | pub description: Option, 26 | pub command: String, 27 | pub group_id: Option, 28 | pub tags: Option, 29 | pub is_favorite: Option, 30 | } 31 | 32 | /// Request to update an existing saved command 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct UpdateSavedCommandRequest { 36 | pub name: Option, 37 | pub description: Option, 38 | pub command: Option, 39 | pub group_id: Option, 40 | pub tags: Option, 41 | pub is_favorite: Option, 42 | } 43 | 44 | impl SavedCommand { 45 | /// Create a new saved command 46 | pub fn new(device_id: String, name: String, command: String, group_id: Option) -> Self { 47 | Self { 48 | base: BaseModel::new(device_id), 49 | name, 50 | description: None, 51 | command, 52 | group_id, 53 | tags: None, 54 | is_favorite: false, 55 | usage_count: 0, 56 | last_used_at: None, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/services/buffer.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./api"; 2 | import { terminalCache } from "../core/performance"; 3 | 4 | /** 5 | * Buffer statistics interface 6 | */ 7 | export interface BufferStats { 8 | totalTerminals: number; 9 | totalLines: number; 10 | memoryUsage: number; 11 | } 12 | 13 | /** 14 | * Get buffer as string from Rust backend 15 | * @param terminalId - Terminal identifier 16 | * @returns Promise of buffer string 17 | */ 18 | export async function getTerminalBuffer(terminalId: string): Promise { 19 | return await api.call("get_terminal_buffer", { 20 | terminalId, 21 | }); 22 | } 23 | 24 | /** 25 | * Check if terminal has buffer in Rust backend (cached) 26 | * @param terminalId - Terminal identifier 27 | * @returns Promise of boolean 28 | */ 29 | export async function hasTerminalBuffer(terminalId: string): Promise { 30 | return await terminalCache.hasTerminalBuffer(terminalId); 31 | } 32 | 33 | /** 34 | * Get buffer statistics from Rust backend (cached) 35 | * @returns Promise of buffer statistics 36 | */ 37 | export async function getBufferStats(): Promise { 38 | const stats = await terminalCache.getBufferStats(); 39 | return { 40 | totalTerminals: stats.totalTerminals || 0, 41 | totalLines: stats.totalLines || 0, 42 | memoryUsage: stats.memoryUsage || 0, 43 | }; 44 | } 45 | 46 | /** 47 | * Cleanup orphaned buffers in Rust backend 48 | * @param activeTerminalIds - Array of active terminal IDs 49 | */ 50 | export async function cleanupTerminalBuffers( 51 | activeTerminalIds: string[], 52 | ): Promise { 53 | return await api.call("cleanup_terminal_buffers", { 54 | activeTerminalIds, 55 | }); 56 | } 57 | 58 | /** 59 | * List all terminals from backend 60 | * @returns Promise of terminal list 61 | */ 62 | export async function listTerminals(): Promise> { 63 | return await api.callRaw>("list_terminals"); 64 | } 65 | -------------------------------------------------------------------------------- /src/components/ssh-profiles/SSHProfileManager.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 65 | -------------------------------------------------------------------------------- /scripts/generate-keystore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ------------------------------------------ 3 | # Script to generate Android keystore for Kerminal 4 | # Written by klpod221 - github.com/klpod221 5 | # ------------------------------------------ 6 | 7 | echo "==========================================" 8 | echo " Kerminal Android Keystore Generator" 9 | echo "==========================================" 10 | echo "" 11 | 12 | KEYSTORE_FILE=".android/kerminal-release.keystore" 13 | 14 | if [[ -f "$KEYSTORE_FILE" ]]; then 15 | echo "⚠️ Keystore already exists at: $KEYSTORE_FILE" 16 | read -p "Do you want to overwrite it? (yes/no): " OVERWRITE 17 | if [[ "$OVERWRITE" != "yes" ]]; then 18 | echo "Aborted." 19 | exit 0 20 | fi 21 | rm "$KEYSTORE_FILE" 22 | fi 23 | 24 | echo "Please enter keystore information:" 25 | echo "" 26 | 27 | read -sp "Store Password: " STORE_PASS 28 | echo "" 29 | read -sp "Key Password: " KEY_PASS 30 | echo "" 31 | echo "" 32 | 33 | echo "Generating keystore..." 34 | keytool -genkey -v -keystore "$KEYSTORE_FILE" \ 35 | -alias kerminal \ 36 | -keyalg RSA \ 37 | -keysize 2048 \ 38 | -validity 10000 \ 39 | -storepass "$STORE_PASS" \ 40 | -keypass "$KEY_PASS" 41 | 42 | if [[ $? -eq 0 ]]; then 43 | echo "" 44 | echo "✅ Keystore created successfully at: $KEYSTORE_FILE" 45 | echo "" 46 | echo "📋 Next steps:" 47 | echo "1. Convert keystore to base64:" 48 | echo " base64 -w 0 $KEYSTORE_FILE" 49 | echo "" 50 | echo "2. Add these secrets to GitHub:" 51 | echo " - ANDROID_KEYSTORE_BASE64: (output from step 1)" 52 | echo " - ANDROID_KEYSTORE_PASSWORD: $STORE_PASS" 53 | echo " - ANDROID_KEY_PASSWORD: $KEY_PASS" 54 | echo " - ANDROID_KEY_ALIAS: kerminal" 55 | echo "" 56 | echo "⚠️ IMPORTANT: Backup this keystore safely!" 57 | echo " If you lose it, you cannot update your app!" 58 | else 59 | echo "" 60 | echo "❌ Failed to create keystore" 61 | exit 1 62 | fi 63 | -------------------------------------------------------------------------------- /src-tauri/src/database/encryption/external_db.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use tokio::sync::RwLock; 3 | 4 | use crate::{ 5 | database::{ 6 | encryption::MasterPasswordManager, 7 | error::{DatabaseResult, EncryptionError}, 8 | traits::EncryptionService, 9 | }, 10 | models::sync::external_db::ConnectionDetails, 11 | }; 12 | 13 | pub struct ExternalDbEncryptor { 14 | master_password_manager: Arc>, 15 | } 16 | 17 | impl ExternalDbEncryptor { 18 | pub fn new(master_password_manager: Arc>) -> Self { 19 | Self { 20 | master_password_manager, 21 | } 22 | } 23 | 24 | pub async fn encrypt_connection_details( 25 | &self, 26 | details: &ConnectionDetails, 27 | ) -> DatabaseResult { 28 | let json = serde_json::to_string(details).map_err(|e| { 29 | EncryptionError::EncryptionFailed(format!( 30 | "Failed to serialize connection details: {}", 31 | e 32 | )) 33 | })?; 34 | 35 | let manager = self.master_password_manager.read().await; 36 | let encrypted = manager.encrypt_string(&json, Some("__shared__")).await?; 37 | 38 | Ok(encrypted) 39 | } 40 | 41 | pub async fn decrypt_connection_details( 42 | &self, 43 | encrypted: &str, 44 | ) -> DatabaseResult { 45 | let manager = self.master_password_manager.read().await; 46 | 47 | let decrypted = match manager.decrypt_string(encrypted, Some("__shared__")).await { 48 | Ok(data) => data, 49 | Err(_) => manager.decrypt_string(encrypted, None).await?, 50 | }; 51 | 52 | let details: ConnectionDetails = serde_json::from_str(&decrypted).map_err(|e| { 53 | EncryptionError::DecryptionFailed(format!( 54 | "Failed to deserialize connection details: {}", 55 | e 56 | )) 57 | })?; 58 | 59 | Ok(details) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/ui/MessageContainer.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 66 | -------------------------------------------------------------------------------- /src-tauri/src/commands/terminal_profile.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppError; 2 | use crate::models::terminal::profile::{ 3 | CreateTerminalProfileRequest, TerminalProfile, UpdateTerminalProfileRequest, 4 | }; 5 | use crate::state::AppState; 6 | use tauri::State; 7 | 8 | #[tauri::command] 9 | pub async fn create_terminal_profile( 10 | app_state: State<'_, AppState>, 11 | request: CreateTerminalProfileRequest, 12 | ) -> Result { 13 | let db = app_state.database_service.lock().await; 14 | db.create_terminal_profile(request) 15 | .await 16 | .map_err(|e| AppError::Database(e.to_string())) 17 | } 18 | 19 | #[tauri::command] 20 | pub async fn get_terminal_profile( 21 | app_state: State<'_, AppState>, 22 | id: String, 23 | ) -> Result { 24 | let db = app_state.database_service.lock().await; 25 | db.get_terminal_profile(&id) 26 | .await 27 | .map_err(|e| AppError::Database(e.to_string())) 28 | } 29 | 30 | #[tauri::command] 31 | pub async fn list_terminal_profiles( 32 | app_state: State<'_, AppState>, 33 | ) -> Result, AppError> { 34 | let db = app_state.database_service.lock().await; 35 | db.list_terminal_profiles() 36 | .await 37 | .map_err(|e| AppError::Database(e.to_string())) 38 | } 39 | 40 | #[tauri::command] 41 | pub async fn update_terminal_profile( 42 | app_state: State<'_, AppState>, 43 | id: String, 44 | request: UpdateTerminalProfileRequest, 45 | ) -> Result { 46 | let db = app_state.database_service.lock().await; 47 | db.update_terminal_profile(&id, request) 48 | .await 49 | .map_err(|e| AppError::Database(e.to_string())) 50 | } 51 | 52 | #[tauri::command] 53 | pub async fn delete_terminal_profile( 54 | app_state: State<'_, AppState>, 55 | id: String, 56 | ) -> Result<(), AppError> { 57 | let db = app_state.database_service.lock().await; 58 | db.delete_terminal_profile(&id) 59 | .await 60 | .map_err(|e| AppError::Database(e.to_string())) 61 | } 62 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "Kerminal" 3 | version = "2.5.5" 4 | description = "Modern Terminal with SSH, Tunneling & Cross-Device Sync" 5 | license = "MIT" 6 | repository = "https://github.com/klpod221/kerminal" 7 | authors = ["klpod221 "] 8 | edition = "2021" 9 | 10 | [lib] 11 | name = "kerminal" 12 | crate-type = ["staticlib", "cdylib", "rlib"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | [build-dependencies] 16 | tauri-build = { version = "2", features = [] } 17 | 18 | [dependencies] 19 | tauri = { version = "2", features = ["unstable"] } 20 | tauri-plugin-opener = "2" 21 | tauri-plugin-process = "2" 22 | serde = { version = "1", features = ["derive"] } 23 | serde_json = "1" 24 | thiserror = "1.0" 25 | sysinfo = "0.37" 26 | tokio = { version = "1", features = ["full"] } 27 | tokio-util = "0.7" 28 | uuid = { version = "1", features = ["v4"] } 29 | russh = "0.45" 30 | russh-keys = "0.45" 31 | russh-config = "0.54" 32 | russh-sftp = "2" 33 | portable-pty = "0.9" 34 | chrono = { version = "0.4", features = ["serde"] } 35 | async-trait = "0.1" 36 | anyhow = "1.0" 37 | regex = "1.5" 38 | gethostname = "0.4" 39 | dirs = "5.0" 40 | 41 | # Database dependencies 42 | sqlx = { version = "0.8", features = [ 43 | "runtime-tokio-rustls", 44 | "sqlite", 45 | "mysql", 46 | "postgres", 47 | "chrono", 48 | "uuid", 49 | ] } 50 | mongodb = "2.8" 51 | bson = "2.8" 52 | 53 | # Encryption dependencies 54 | aes-gcm = "0.10" 55 | argon2 = "0.5" 56 | rand = "0.8" 57 | base64 = "0.22" 58 | pbkdf2 = "0.12" 59 | hmac = "0.12" 60 | sha2 = "0.10" 61 | 62 | # Keychain integration 63 | keyring = "2.3" 64 | 65 | # Additional utilities 66 | futures = "0.3" 67 | dashmap = "5.5" 68 | tauri-plugin-clipboard-manager = "2" 69 | tauri-plugin-store = "2" 70 | tauri-plugin-dialog = "2" 71 | tauri-plugin-fs = "2" 72 | walkdir = "2.5.0" 73 | reqwest = { version = "0.12.25", features = ["json", "rustls-tls"] } 74 | 75 | [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] 76 | tauri-plugin-updater = "2" 77 | -------------------------------------------------------------------------------- /src-tauri/src/models/history/history.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | /// A single command history entry 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct CommandHistoryEntry { 8 | /// The command text 9 | pub command: String, 10 | /// Timestamp when the command was executed (if available) 11 | #[serde(skip_serializing_if = "Option::is_none")] 12 | pub timestamp: Option>, 13 | /// Index in the history (for ordering) 14 | pub index: usize, 15 | } 16 | 17 | /// Request to get history for a terminal 18 | #[derive(Debug, Clone, Serialize, Deserialize)] 19 | #[serde(rename_all = "camelCase")] 20 | pub struct GetTerminalHistoryRequest { 21 | pub terminal_id: String, 22 | /// Maximum number of entries to return (0 = all) 23 | #[serde(skip_serializing_if = "Option::is_none")] 24 | pub limit: Option, 25 | } 26 | 27 | /// Request to search history 28 | #[derive(Debug, Clone, Serialize, Deserialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct SearchHistoryRequest { 31 | pub terminal_id: String, 32 | /// Search query (case-insensitive) 33 | pub query: String, 34 | /// Maximum number of results to return (0 = all) 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub limit: Option, 37 | } 38 | 39 | /// Response for search history 40 | #[derive(Debug, Clone, Serialize, Deserialize)] 41 | #[serde(rename_all = "camelCase")] 42 | pub struct SearchHistoryResponse { 43 | pub entries: Vec, 44 | pub total_count: usize, 45 | } 46 | 47 | /// Request to export history 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | #[serde(rename_all = "camelCase")] 50 | pub struct ExportHistoryRequest { 51 | pub terminal_id: String, 52 | /// Export format: "json" or "txt" 53 | pub format: String, 54 | /// File path to export to 55 | pub file_path: String, 56 | /// Optional search query to filter before export 57 | #[serde(skip_serializing_if = "Option::is_none")] 58 | pub query: Option, 59 | } 60 | -------------------------------------------------------------------------------- /src-tauri/src/database/encryption/aes.rs: -------------------------------------------------------------------------------- 1 | use crate::database::error::{EncryptionError, EncryptionResult}; 2 | use aes_gcm::{ 3 | aead::{Aead, KeyInit, OsRng}, 4 | Aes256Gcm, Nonce, 5 | }; 6 | use rand::RngCore; 7 | 8 | /// AES-256-GCM encryption service 9 | pub struct AESEncryption; 10 | 11 | impl AESEncryption { 12 | /// Encrypt data using AES-256-GCM 13 | pub fn encrypt(key: &[u8; 32], data: &[u8]) -> EncryptionResult> { 14 | let cipher = Aes256Gcm::new_from_slice(key) 15 | .map_err(|e| EncryptionError::InvalidKey(e.to_string()))?; 16 | 17 | let mut nonce_bytes = [0u8; 12]; 18 | OsRng.fill_bytes(&mut nonce_bytes); 19 | let nonce = &Nonce::from(nonce_bytes); 20 | 21 | let ciphertext = cipher 22 | .encrypt(nonce, data) 23 | .map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?; 24 | 25 | let mut result = Vec::with_capacity(12 + ciphertext.len()); 26 | result.extend_from_slice(&nonce_bytes); 27 | result.extend_from_slice(&ciphertext); 28 | 29 | Ok(result) 30 | } 31 | 32 | /// Decrypt data using AES-256-GCM 33 | pub fn decrypt(key: &[u8; 32], encrypted_data: &[u8]) -> EncryptionResult> { 34 | if encrypted_data.len() < 12 { 35 | return Err(EncryptionError::InvalidFormat); 36 | } 37 | 38 | let cipher = Aes256Gcm::new_from_slice(key) 39 | .map_err(|e| EncryptionError::InvalidKey(e.to_string()))?; 40 | 41 | let (nonce_bytes, ciphertext) = encrypted_data.split_at(12); 42 | let nonce = &Nonce::from(*<&[u8; 12]>::try_from(nonce_bytes).unwrap()); 43 | 44 | let plaintext = cipher 45 | .decrypt(nonce, ciphertext) 46 | .map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))?; 47 | 48 | Ok(plaintext) 49 | } 50 | 51 | /// Generate a random 256-bit key 52 | pub fn generate_key() -> [u8; 32] { 53 | let mut key = [0u8; 32]; 54 | OsRng.fill_bytes(&mut key); 55 | key 56 | } 57 | 58 | /// Generate a random salt 59 | pub fn generate_salt() -> [u8; 32] { 60 | Self::generate_key() // Same as key generation 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/composables/useKeyboardShortcuts.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onBeforeUnmount, type Ref } from "vue"; 2 | 3 | /** 4 | * Keyboard shortcuts configuration 5 | */ 6 | interface KeyboardShortcutConfig { 7 | key: string; 8 | ctrlKey?: boolean; 9 | altKey?: boolean; 10 | shiftKey?: boolean; 11 | metaKey?: boolean; 12 | preventDefault?: boolean; 13 | action: () => void; 14 | } 15 | 16 | /** 17 | * Composable for managing keyboard shortcuts 18 | * @param shortcuts - Array of keyboard shortcut configurations 19 | * @param enabled - Ref to control if shortcuts are enabled 20 | */ 21 | export function useKeyboardShortcuts( 22 | shortcuts: KeyboardShortcutConfig[], 23 | enabled?: Ref, 24 | ) { 25 | const handleKeydown = (event: KeyboardEvent): void => { 26 | if (enabled && !enabled.value) return; 27 | 28 | const target = event.target as HTMLElement; 29 | const isInputElement = 30 | target.tagName === "INPUT" || 31 | target.tagName === "TEXTAREA" || 32 | target.contentEditable === "true"; 33 | 34 | if (isInputElement && !target.classList.contains("xterm-helper-textarea")) { 35 | return; 36 | } 37 | 38 | for (const shortcut of shortcuts) { 39 | const keyMatches = event.key.toLowerCase() === shortcut.key.toLowerCase(); 40 | const ctrlMatches = !!shortcut.ctrlKey === event.ctrlKey; 41 | const altMatches = !!shortcut.altKey === event.altKey; 42 | const shiftMatches = !!shortcut.shiftKey === event.shiftKey; 43 | const metaMatches = !!shortcut.metaKey === event.metaKey; 44 | 45 | if ( 46 | keyMatches && 47 | ctrlMatches && 48 | altMatches && 49 | shiftMatches && 50 | metaMatches 51 | ) { 52 | if (shortcut.preventDefault !== false) { 53 | event.preventDefault(); 54 | event.stopPropagation(); 55 | } 56 | shortcut.action(); 57 | break; 58 | } 59 | } 60 | }; 61 | 62 | onMounted(() => { 63 | document.addEventListener("keydown", handleKeydown); 64 | }); 65 | 66 | onBeforeUnmount(() => { 67 | document.removeEventListener("keydown", handleKeydown); 68 | }); 69 | 70 | return { 71 | handleKeydown, 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src-tauri/src/models/sftp/error.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use thiserror::Error; 3 | 4 | /// SFTP-specific errors 5 | #[derive(Debug, Error, Clone, Serialize, Deserialize)] 6 | #[serde(rename_all = "camelCase")] 7 | pub enum SFTPError { 8 | /// Session not found 9 | #[error("SFTP session not found: {session_id}")] 10 | SessionNotFound { session_id: String }, 11 | 12 | /// Failed to establish SFTP session 13 | #[error("Failed to establish SFTP session: {message}")] 14 | SessionFailed { message: String }, 15 | 16 | /// File or directory not found 17 | #[error("File not found: {path}")] 18 | FileNotFound { path: String }, 19 | 20 | /// Permission denied 21 | #[error("Permission denied: {path}")] 22 | PermissionDenied { path: String }, 23 | 24 | /// File already exists 25 | #[error("File already exists: {path}")] 26 | FileExists { path: String }, 27 | 28 | /// Invalid path 29 | #[error("Invalid path: {path}")] 30 | InvalidPath { path: String }, 31 | 32 | /// Transfer not found 33 | #[error("Transfer not found: {transfer_id}")] 34 | TransferNotFound { transfer_id: String }, 35 | 36 | /// Transfer already in progress or completed 37 | #[error("Transfer {transfer_id} is not in a resumable state")] 38 | TransferNotResumable { transfer_id: String }, 39 | 40 | /// I/O error 41 | #[error("I/O error: {message}")] 42 | IoError { message: String }, 43 | 44 | /// Remote server error 45 | #[error("Remote server error: {message}")] 46 | RemoteError { message: String }, 47 | 48 | /// Connection lost 49 | #[error("Connection lost: {message}")] 50 | ConnectionLost { message: String }, 51 | 52 | /// Generic error 53 | #[error("SFTP error: {message}")] 54 | Other { message: String }, 55 | } 56 | 57 | impl From for SFTPError { 58 | fn from(err: anyhow::Error) -> Self { 59 | SFTPError::Other { 60 | message: err.to_string(), 61 | } 62 | } 63 | } 64 | 65 | impl From for SFTPError { 66 | fn from(err: std::io::Error) -> Self { 67 | SFTPError::IoError { 68 | message: err.to_string(), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/types/savedCommand.ts: -------------------------------------------------------------------------------- 1 | export interface BaseModel { 2 | id: string; 3 | createdAt: string; 4 | updatedAt: string; 5 | deviceId: string; 6 | version: number; 7 | syncStatus: "synced" | "pending" | "conflict"; 8 | } 9 | 10 | export interface SavedCommand extends BaseModel { 11 | name: string; 12 | description?: string; 13 | command: string; 14 | groupId?: string; 15 | tags?: string; // JSON array as string 16 | isFavorite: boolean; 17 | usageCount: number; 18 | lastUsedAt?: string; 19 | } 20 | 21 | export interface SavedCommandGroup extends BaseModel { 22 | name: string; 23 | description?: string; 24 | color?: string; 25 | icon?: string; 26 | } 27 | 28 | export interface CreateSavedCommandRequest { 29 | name: string; 30 | description?: string; 31 | command: string; 32 | groupId?: string; 33 | tags?: string; 34 | isFavorite?: boolean; 35 | } 36 | 37 | export interface UpdateSavedCommandRequest { 38 | name?: string; 39 | description?: string; 40 | command?: string; 41 | groupId?: string; 42 | tags?: string; 43 | isFavorite?: boolean; 44 | } 45 | 46 | export interface CreateSavedCommandGroupRequest { 47 | name: string; 48 | description?: string; 49 | color?: string; 50 | icon?: string; 51 | } 52 | 53 | export interface UpdateSavedCommandGroupRequest { 54 | name?: string; 55 | description?: string; 56 | color?: string; 57 | icon?: string; 58 | } 59 | 60 | export interface SavedCommandGroupWithStats extends SavedCommandGroup { 61 | commandCount: number; 62 | } 63 | 64 | export interface SavedCommandWithParsedTags extends SavedCommand { 65 | parsedTags: string[]; 66 | } 67 | 68 | export interface GroupedSavedCommandsData { 69 | group?: SavedCommandGroup; 70 | commands: SavedCommand[]; 71 | commandCount: number; 72 | } 73 | 74 | export type SavedCommandSortBy = 75 | | "name" 76 | | "lastUsed" 77 | | "usageCount" 78 | | "createdAt" 79 | | "updatedAt"; 80 | 81 | export type SavedCommandFilterBy = "all" | "favorites" | "recent" | "unused"; 82 | 83 | export interface SavedCommandSearchParams { 84 | query?: string; 85 | groupId?: string; 86 | sortBy?: SavedCommandSortBy; 87 | sortOrder?: "asc" | "desc"; 88 | filterBy?: SavedCommandFilterBy; 89 | tags?: string[]; 90 | } 91 | -------------------------------------------------------------------------------- /src-tauri/src/commands/auth_events.rs: -------------------------------------------------------------------------------- 1 | use crate::core::auth_session_manager::SessionLockReason; 2 | use crate::state::AppState; 3 | use chrono::Utc; 4 | use serde_json::json; 5 | use tauri::State; 6 | 7 | /// Manually trigger auth session unlock notification (called after successful unlock) 8 | #[tauri::command] 9 | pub async fn notify_session_unlocked(state: State<'_, AppState>) -> Result<(), String> { 10 | let auth_session_manager = state.auth_session_manager.lock().await; 11 | auth_session_manager 12 | .on_session_unlocked() 13 | .await 14 | .map_err(|e| e.to_string())?; 15 | 16 | Ok(()) 17 | } 18 | 19 | /// Manually trigger auth session lock notification (called after manual lock) 20 | #[tauri::command] 21 | pub async fn notify_session_locked( 22 | state: State<'_, AppState>, 23 | reason: Option, 24 | ) -> Result<(), String> { 25 | let lock_reason = match reason.as_deref() { 26 | Some("manual") => SessionLockReason::Manual, 27 | Some("timeout") => SessionLockReason::Timeout, 28 | Some(other) => SessionLockReason::Error(other.to_string()), 29 | None => SessionLockReason::Manual, 30 | }; 31 | 32 | let auth_session_manager = state.auth_session_manager.lock().await; 33 | auth_session_manager 34 | .on_session_locked(lock_reason) 35 | .await 36 | .map_err(|e| e.to_string())?; 37 | 38 | Ok(()) 39 | } 40 | 41 | /// Get current auth session status 42 | #[tauri::command] 43 | pub async fn get_auth_session_status( 44 | state: State<'_, AppState>, 45 | ) -> Result { 46 | let database_service = state.database_service.lock().await; 47 | let status = database_service 48 | .get_master_password_status() 49 | .await 50 | .map_err(|e| e.to_string())?; 51 | 52 | Ok(json!({ 53 | "isSetup": status.is_setup, 54 | "isUnlocked": status.is_unlocked, 55 | "autoUnlockEnabled": status.auto_unlock_enabled, 56 | "keychainAvailable": status.keychain_available, 57 | "sessionActive": status.session_active, 58 | "sessionExpiresAt": status.session_expires_at, 59 | "loadedDeviceCount": status.loaded_device_count, 60 | "timestamp": Utc::now() 61 | })) 62 | } 63 | -------------------------------------------------------------------------------- /src/components/sftp/FileDeleteModal.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 81 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "Kerminal", 4 | "version": "2.5.5", 5 | "identifier": "com.klpod221.kerminal", 6 | "build": { 7 | "beforeDevCommand": "npm run dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "npm run build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "Kerminal", 16 | "width": 1200, 17 | "height": 800 18 | } 19 | ], 20 | "security": { 21 | "capabilities": ["default", "clipboard"], 22 | "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' asset: https://asset.localhost blob: data:; font-src 'self'; connect-src 'self' https://github.com/klpod221/kerminal/releases/latest/download/latest.json" 23 | } 24 | }, 25 | "plugins": { 26 | "updater": { 27 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEEzRjRDNkU5NUU5MTY5QTkKUldTcGFaRmU2Y2IwbytSMi9kT0gzK3FxbnIvektEcE9ZOER2SUt1WkNWL1hKb0JiMUcwaWJQQ3oK", 28 | "endpoints": [ 29 | "https://github.com/klpod221/kerminal/releases/latest/download/latest.json" 30 | ], 31 | "windows": { 32 | "installMode": "passive" 33 | } 34 | }, 35 | "process": { 36 | "all": true 37 | }, 38 | "fs": { 39 | "requireLiteralLeadingDot": false 40 | } 41 | }, 42 | "bundle": { 43 | "createUpdaterArtifacts": true, 44 | "publisher": "klpod221", 45 | "license": "MIT", 46 | "copyright": "klpod221", 47 | "homepage": "https://klpod221.com/kerminal", 48 | "active": true, 49 | "category": "Developer Tools", 50 | "targets": "all", 51 | "icon": [ 52 | "icons/32x32.png", 53 | "icons/128x128.png", 54 | "icons/128x128@2x.png", 55 | "icons/icon.icns", 56 | "icons/icon.ico" 57 | ], 58 | "longDescription": "A powerful, feature-rich terminal emulator with advanced SSH management, multi-device sync, and enterprise-grade encryption.", 59 | "shortDescription": "Modern Terminal Emulator & SSH Manager", 60 | "macOS": { 61 | "minimumSystemVersion": "13.0", 62 | "exceptionDomain": "", 63 | "entitlements": null, 64 | "frameworks": [] 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/validators.ts: -------------------------------------------------------------------------------- 1 | type FormValues = { [key: string]: unknown }; 2 | 3 | type ValidatorFn = (value: unknown, params: string[], allValues: FormValues) => string | undefined; 4 | 5 | const validators: Record = { 6 | required: (value) => { 7 | if ( 8 | value === null || 9 | value === undefined || 10 | (typeof value === "string" && value.trim() === "") || 11 | (Array.isArray(value) && value.length === 0) 12 | ) { 13 | return "This field is required."; 14 | } 15 | }, 16 | min: (value, params) => { 17 | if (String(value).length < Number(params[0])) { 18 | return `Must be at least ${params[0]} characters.`; 19 | } 20 | }, 21 | max: (value, params) => { 22 | if (String(value).length > Number(params[0])) { 23 | return `Must not exceed ${params[0]} characters.`; 24 | } 25 | }, 26 | between: (value, params) => { 27 | const len = String(value).length; 28 | if (len < Number(params[0]) || len > Number(params[1])) { 29 | return `Must be between ${params[0]} and ${params[1]} characters.`; 30 | } 31 | }, 32 | password: (value) => { 33 | const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/; 34 | if (!passwordRegex.test(String(value))) { 35 | return "Password must be at least 8 characters long and include uppercase, lowercase letters, and numbers."; 36 | } 37 | }, 38 | same: (value, params, allValues) => { 39 | const otherValue = allValues[params[0]]; 40 | if (value !== otherValue) { 41 | return `Values must match with ${params[0]}.`; 42 | } 43 | }, 44 | different: (value, params, allValues) => { 45 | const otherValue = allValues[params[0]]; 46 | if (value === otherValue) { 47 | return `Values must be different from ${params[0]}.`; 48 | } 49 | }, 50 | }; 51 | 52 | export function validate( 53 | value: unknown, 54 | rules: string, 55 | allValues: FormValues, 56 | ): string { 57 | const ruleParts = rules.split("|"); 58 | 59 | for (const rule of ruleParts) { 60 | if (!rule) continue; 61 | 62 | const [ruleName, ...params] = rule.split(":"); 63 | const validator = validators[ruleName]; 64 | 65 | if (validator) { 66 | const error = validator(value, params, allValues); 67 | if (error) return error; 68 | } 69 | } 70 | 71 | return ""; // No errors 72 | } 73 | -------------------------------------------------------------------------------- /src/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @import "./fonts.css"; 2 | @import "./animations.css"; 3 | @import "tailwindcss"; 4 | 5 | @theme { 6 | /* Custom background colors */ 7 | --color-bg-primary: #0d0d0d; 8 | --color-bg-secondary: #171717; 9 | --color-bg-tertiary: #1a1a1a; 10 | --color-bg-quaternary: #0a0a0a; 11 | 12 | /* Scrollbar colors */ 13 | --color-scrollbar-thumb: #888888; 14 | --color-scrollbar-thumb-hover: #555555; 15 | --color-scrollbar-thumb-mobile: #666666; 16 | 17 | /* Accent colors */ 18 | --color-accent-blue: #74c7ec; 19 | 20 | /* Border colors */ 21 | --color-border-dark: #1f1f1f; 22 | } 23 | 24 | html, 25 | body { 26 | background-color: transparent; 27 | margin: 0; 28 | padding: 0; 29 | overflow: hidden; 30 | font-family: "FiraCode Nerd Font", "JetBrains Mono", monospace; 31 | } 32 | 33 | :root { 34 | --ui-zoom: 1; 35 | } 36 | 37 | @media (min-resolution: 144dpi), (-webkit-min-device-pixel-ratio: 1.5) { 38 | :root { 39 | --ui-zoom: 1.2; 40 | } 41 | 42 | html { 43 | zoom: var(--ui-zoom); 44 | } 45 | 46 | .terminal-container { 47 | zoom: calc(1 / var(--ui-zoom)); 48 | } 49 | } 50 | 51 | /* Custom backdrop blur for overlay */ 52 | .backdrop-blur { 53 | backdrop-filter: blur(4px); 54 | -webkit-backdrop-filter: blur(4px); 55 | } 56 | 57 | /* Scrollbar styles */ 58 | ::-webkit-scrollbar { 59 | width: 6px; 60 | height: 6px; 61 | } 62 | 63 | ::-webkit-scrollbar-thumb { 64 | background-color: var(--color-scrollbar-thumb); 65 | border-radius: 4px; 66 | } 67 | 68 | ::-webkit-scrollbar-thumb:hover { 69 | background-color: var(--color-scrollbar-thumb-hover); 70 | } 71 | 72 | /* Touch-friendly scrollbar for mobile */ 73 | @media (max-width: 640px) { 74 | ::-webkit-scrollbar { 75 | width: 8px; 76 | height: 8px; 77 | } 78 | 79 | ::-webkit-scrollbar-thumb { 80 | background-color: var(--color-scrollbar-thumb-mobile); 81 | border-radius: 6px; 82 | } 83 | } 84 | 85 | .draggable { 86 | -webkit-app-region: drag; 87 | } 88 | 89 | .no-drag { 90 | -webkit-app-region: no-drag; 91 | } 92 | 93 | /* Mobile touch optimizations */ 94 | @media (max-width: 640px) { 95 | input, 96 | select, 97 | textarea { 98 | font-size: 16px; 99 | } 100 | } 101 | 102 | /* Xterm */ 103 | .xterm { 104 | height: 100%; 105 | } 106 | -------------------------------------------------------------------------------- /src-tauri/src/commands/buffer.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AppError; 2 | use crate::models::buffer::{ 3 | CleanupTerminalBuffersRequest, GetTerminalBufferChunkRequest, GetTerminalBufferRequest, 4 | HasTerminalBufferRequest, TerminalBufferChunk, 5 | }; 6 | use crate::services::buffer_manager::BufferStats; 7 | use crate::state::AppState; 8 | use tauri::State; 9 | 10 | /// Get buffer as string for a terminal 11 | #[tauri::command] 12 | pub async fn get_terminal_buffer( 13 | request: GetTerminalBufferRequest, 14 | app_state: State<'_, AppState>, 15 | ) -> Result { 16 | let buffer_manager = app_state.terminal_manager.get_buffer_manager(); 17 | let buffer_string = buffer_manager.get_buffer_string(&request.terminal_id).await; 18 | Ok(buffer_string.unwrap_or_default()) 19 | } 20 | 21 | /// Get buffer chunk for a terminal 22 | #[tauri::command] 23 | pub async fn get_terminal_buffer_chunk( 24 | request: GetTerminalBufferChunkRequest, 25 | app_state: State<'_, AppState>, 26 | ) -> Result { 27 | let buffer_manager = app_state.terminal_manager.get_buffer_manager(); 28 | let chunk = buffer_manager 29 | .get_buffer_chunk(&request.terminal_id, request.start_line, request.chunk_size) 30 | .await; 31 | 32 | Ok(chunk) 33 | } 34 | 35 | /// Check if terminal has buffer 36 | #[tauri::command] 37 | pub async fn has_terminal_buffer( 38 | request: HasTerminalBufferRequest, 39 | app_state: State<'_, AppState>, 40 | ) -> Result { 41 | let buffer_manager = app_state.terminal_manager.get_buffer_manager(); 42 | let has_buffer = buffer_manager.has_buffer(&request.terminal_id).await; 43 | Ok(has_buffer) 44 | } 45 | 46 | /// Get buffer statistics 47 | #[tauri::command] 48 | pub async fn get_buffer_stats(app_state: State<'_, AppState>) -> Result { 49 | let buffer_manager = app_state.terminal_manager.get_buffer_manager(); 50 | let stats = buffer_manager.get_stats().await; 51 | Ok(stats) 52 | } 53 | 54 | /// Cleanup orphaned buffers 55 | #[tauri::command] 56 | pub async fn cleanup_terminal_buffers( 57 | request: CleanupTerminalBuffersRequest, 58 | app_state: State<'_, AppState>, 59 | ) -> Result<(), AppError> { 60 | let buffer_manager = app_state.terminal_manager.get_buffer_manager(); 61 | buffer_manager 62 | .cleanup_orphaned_buffers(&request.active_terminal_ids) 63 | .await; 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /src/composables/useFormStyles.ts: -------------------------------------------------------------------------------- 1 | import { computed, type ComputedRef } from "vue"; 2 | 3 | /** 4 | * Props interface for form styles composable 5 | */ 6 | export interface FormStylesProps { 7 | size?: "sm" | "md" | "lg"; 8 | errorMessage?: string; 9 | disabled?: boolean; 10 | readonly?: boolean; 11 | } 12 | 13 | /** 14 | * Return type for useFormStyles composable 15 | */ 16 | export interface UseFormStylesReturn { 17 | sizeClasses: ComputedRef; 18 | stateClasses: ComputedRef; 19 | iconSize: ComputedRef; 20 | } 21 | 22 | /** 23 | * Composable for shared form field styling logic 24 | * Handles size classes, state classes, and icon sizing 25 | * 26 | * @param props - Component props containing styling configuration 27 | * @returns Object containing computed style classes 28 | * 29 | * @example 30 | * ```vue 31 | * 35 | * ``` 36 | */ 37 | export function useFormStyles(props: FormStylesProps): UseFormStylesReturn { 38 | /** 39 | * Size-based CSS classes 40 | */ 41 | const sizeClasses = computed(() => { 42 | switch (props.size) { 43 | case "sm": 44 | return "text-sm py-1.5"; 45 | case "lg": 46 | return "text-lg py-3"; 47 | default: 48 | return "text-base py-2"; 49 | } 50 | }); 51 | 52 | /** 53 | * State-based CSS classes (error, disabled, readonly, default) 54 | */ 55 | const stateClasses = computed(() => { 56 | if (props.errorMessage) { 57 | return "border-red-500 bg-red-500/5 text-white focus:border-red-400"; 58 | } 59 | 60 | if (props.disabled) { 61 | return "border-gray-600 bg-gray-800 text-gray-400"; 62 | } 63 | 64 | if (props.readonly) { 65 | return "border-gray-600 bg-gray-700 text-gray-300"; 66 | } 67 | 68 | return "border-gray-600 bg-gray-800 text-white placeholder-gray-400 hover:border-gray-500 focus:border-blue-500 focus:ring-blue-500"; 69 | }); 70 | 71 | /** 72 | * Icon size based on component size 73 | */ 74 | const iconSize = computed(() => { 75 | switch (props.size) { 76 | case "sm": 77 | return 16; 78 | case "lg": 79 | return 20; 80 | default: 81 | return 18; 82 | } 83 | }); 84 | 85 | return { 86 | sizeClasses, 87 | stateClasses, 88 | iconSize, 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /src/components/ui/PasswordStrength.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 100 | -------------------------------------------------------------------------------- /src/services/sshKey.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./api"; 2 | import type { 3 | SSHKey, 4 | CreateSSHKeyRequest, 5 | UpdateSSHKeyRequest, 6 | } from "../types/ssh"; 7 | 8 | /** 9 | * Create a new SSH key 10 | * @param request - Create SSH key request 11 | * @returns Created SSH key 12 | */ 13 | export async function createSSHKey( 14 | request: CreateSSHKeyRequest, 15 | ): Promise { 16 | return await api.call("create_ssh_key", request); 17 | } 18 | 19 | /** 20 | * Get all SSH keys 21 | * @returns List of all SSH keys 22 | */ 23 | export async function getSSHKeys(): Promise { 24 | return await api.call("get_ssh_keys"); 25 | } 26 | 27 | /** 28 | * Get SSH key by ID 29 | * @param id - SSH key ID 30 | * @returns SSH key details 31 | */ 32 | export async function getSSHKey(id: string): Promise { 33 | return await api.callRaw("get_ssh_key", id); 34 | } 35 | 36 | /** 37 | * Update SSH key (metadata only - name, description) 38 | * @param id - SSH key ID 39 | * @param request - Update request 40 | * @returns Updated SSH key 41 | */ 42 | export async function updateSSHKey( 43 | id: string, 44 | request: UpdateSSHKeyRequest, 45 | ): Promise { 46 | return await api.callRaw("update_ssh_key", id, request); 47 | } 48 | 49 | /** 50 | * Delete SSH key 51 | * @param id - SSH key ID 52 | * @param force - Force deletion even if profiles are using it 53 | */ 54 | export async function deleteSSHKey(id: string, force = false): Promise { 55 | return await api.callRaw("delete_ssh_key", id, force); 56 | } 57 | 58 | /** 59 | * Count how many profiles are using a specific key 60 | * @param keyId - SSH key ID 61 | * @returns Number of profiles using the key 62 | */ 63 | export async function countProfilesUsingKey(keyId: string): Promise { 64 | return await api.callRaw("count_profiles_using_key", keyId); 65 | } 66 | 67 | /** 68 | * Import SSH key from file 69 | * @param name - Display name for the key 70 | * @param filePath - Path to the private key file 71 | * @param passphrase - Optional passphrase if key is encrypted 72 | * @param description - Optional description 73 | * @returns Imported SSH key 74 | */ 75 | export async function importSSHKeyFromFile( 76 | name: string, 77 | filePath: string, 78 | passphrase?: string, 79 | description?: string, 80 | ): Promise { 81 | return await api.callRaw( 82 | "import_ssh_key_from_file", 83 | name, 84 | filePath, 85 | passphrase, 86 | description, 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/components/auth/MasterPasswordUnlock.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 86 | -------------------------------------------------------------------------------- /src/components/ssh-profiles/SSHConfigHostItem.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 87 | -------------------------------------------------------------------------------- /src-tauri/src/models/sftp/transfer.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Transfer progress information 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct TransferProgress { 7 | /// Unique transfer ID 8 | pub transfer_id: String, 9 | /// Transfer status 10 | pub status: TransferStatus, 11 | /// Transfer direction (upload or download) 12 | pub direction: TransferDirection, 13 | /// Local file path 14 | pub local_path: String, 15 | /// Remote file path 16 | pub remote_path: String, 17 | /// Total size in bytes 18 | pub total_bytes: u64, 19 | /// Transferred bytes so far 20 | pub transferred_bytes: u64, 21 | /// Transfer speed in bytes per second 22 | pub speed_bytes_per_sec: Option, 23 | /// Estimated time remaining in seconds 24 | pub eta_seconds: Option, 25 | /// Error message if transfer failed 26 | pub error: Option, 27 | /// Timestamp when transfer started 28 | pub started_at: chrono::DateTime, 29 | /// Timestamp when transfer completed (or None if still in progress) 30 | pub completed_at: Option>, 31 | /// Transfer priority (0-255, higher = higher priority) 32 | pub priority: u8, 33 | /// Number of retry attempts made 34 | pub retry_count: u32, 35 | /// Maximum number of retry attempts allowed 36 | pub max_retries: u32, 37 | /// Timestamp for next retry attempt (for exponential backoff) 38 | pub next_retry_at: Option>, 39 | } 40 | 41 | /// Transfer status 42 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 43 | #[serde(rename_all = "lowercase")] 44 | pub enum TransferStatus { 45 | /// Transfer is queued 46 | Queued, 47 | /// Transfer is in progress 48 | InProgress, 49 | /// Transfer is paused 50 | Paused, 51 | /// Transfer completed successfully 52 | Completed, 53 | /// Transfer failed 54 | Failed, 55 | /// Transfer was cancelled 56 | Cancelled, 57 | } 58 | 59 | /// Transfer direction 60 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 61 | #[serde(rename_all = "lowercase")] 62 | pub enum TransferDirection { 63 | /// Uploading from local to remote 64 | Upload, 65 | /// Downloading from remote to local 66 | Download, 67 | } 68 | 69 | impl TransferProgress { 70 | /// Check if transfer is active (queued or in progress) 71 | pub fn is_active(&self) -> bool { 72 | matches!( 73 | self.status, 74 | TransferStatus::Queued | TransferStatus::InProgress 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/components/ssh-profiles/RecentConnectionItem.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 83 | -------------------------------------------------------------------------------- /src/components/recording/RecordingControls.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 85 | -------------------------------------------------------------------------------- /src/components/ssh-profiles/SSHProfileItem.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 90 | -------------------------------------------------------------------------------- /src-tauri/src/services/sync/serializer.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | 3 | use crate::database::error::{DatabaseError, DatabaseResult}; 4 | use crate::models::saved_command::{SavedCommand, SavedCommandGroup}; 5 | use crate::models::ssh::{SSHGroup, SSHKey, SSHProfile, SSHTunnel}; 6 | 7 | /// Helper trait for converting models to/from sync records 8 | pub trait SyncSerializable { 9 | fn to_json(&self) -> DatabaseResult; 10 | fn from_json(value: &Value) -> DatabaseResult 11 | where 12 | Self: Sized; 13 | } 14 | 15 | impl SyncSerializable for SSHProfile { 16 | fn to_json(&self) -> DatabaseResult { 17 | serde_json::to_value(self).map_err(DatabaseError::SerializationError) 18 | } 19 | 20 | fn from_json(value: &Value) -> DatabaseResult { 21 | serde_json::from_value(value.clone()).map_err(DatabaseError::SerializationError) 22 | } 23 | } 24 | 25 | impl SyncSerializable for SSHGroup { 26 | fn to_json(&self) -> DatabaseResult { 27 | serde_json::to_value(self).map_err(DatabaseError::SerializationError) 28 | } 29 | 30 | fn from_json(value: &Value) -> DatabaseResult { 31 | serde_json::from_value(value.clone()).map_err(DatabaseError::SerializationError) 32 | } 33 | } 34 | 35 | impl SyncSerializable for SSHKey { 36 | fn to_json(&self) -> DatabaseResult { 37 | serde_json::to_value(self).map_err(DatabaseError::SerializationError) 38 | } 39 | 40 | fn from_json(value: &Value) -> DatabaseResult { 41 | serde_json::from_value(value.clone()).map_err(DatabaseError::SerializationError) 42 | } 43 | } 44 | 45 | impl SyncSerializable for SSHTunnel { 46 | fn to_json(&self) -> DatabaseResult { 47 | serde_json::to_value(self).map_err(DatabaseError::SerializationError) 48 | } 49 | 50 | fn from_json(value: &Value) -> DatabaseResult { 51 | serde_json::from_value(value.clone()).map_err(DatabaseError::SerializationError) 52 | } 53 | } 54 | 55 | impl SyncSerializable for SavedCommand { 56 | fn to_json(&self) -> DatabaseResult { 57 | serde_json::to_value(self).map_err(DatabaseError::SerializationError) 58 | } 59 | 60 | fn from_json(value: &Value) -> DatabaseResult { 61 | serde_json::from_value(value.clone()).map_err(DatabaseError::SerializationError) 62 | } 63 | } 64 | 65 | impl SyncSerializable for SavedCommandGroup { 66 | fn to_json(&self) -> DatabaseResult { 67 | serde_json::to_value(self).map_err(DatabaseError::SerializationError) 68 | } 69 | 70 | fn from_json(value: &Value) -> DatabaseResult { 71 | serde_json::from_value(value.clone()).map_err(DatabaseError::SerializationError) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/components/auth/PasswordConfirmModal.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 97 | -------------------------------------------------------------------------------- /src-tauri/src/models/sync/progress.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// Sync progress event for real-time updates 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | #[serde(rename_all = "camelCase")] 6 | pub struct SyncProgressEvent { 7 | /// Type of sync: "sftp" or "database" 8 | pub sync_type: String, 9 | /// Current operation: "comparing", "uploading", "downloading", "syncing" 10 | pub operation: String, 11 | /// Current file/table being processed 12 | pub current_item: String, 13 | /// Number of items processed 14 | pub processed: u32, 15 | /// Total number of items 16 | pub total: u32, 17 | /// Status: "in_progress", "completed", "error" 18 | pub status: String, 19 | /// Error message if any 20 | pub error: Option, 21 | } 22 | 23 | impl SyncProgressEvent { 24 | pub fn sftp_progress(operation: &str, item: &str, processed: u32, total: u32) -> Self { 25 | Self { 26 | sync_type: "sftp".to_string(), 27 | operation: operation.to_string(), 28 | current_item: item.to_string(), 29 | processed, 30 | total, 31 | status: "in_progress".to_string(), 32 | error: None, 33 | } 34 | } 35 | 36 | pub fn sftp_completed(total: u32) -> Self { 37 | Self { 38 | sync_type: "sftp".to_string(), 39 | operation: "completed".to_string(), 40 | current_item: String::new(), 41 | processed: total, 42 | total, 43 | status: "completed".to_string(), 44 | error: None, 45 | } 46 | } 47 | 48 | pub fn sftp_error(error: &str) -> Self { 49 | Self { 50 | sync_type: "sftp".to_string(), 51 | operation: "error".to_string(), 52 | current_item: String::new(), 53 | processed: 0, 54 | total: 0, 55 | status: "error".to_string(), 56 | error: Some(error.to_string()), 57 | } 58 | } 59 | 60 | #[allow(dead_code)] 61 | pub fn database_progress(table: &str, processed: u32, total: u32) -> Self { 62 | Self { 63 | sync_type: "database".to_string(), 64 | operation: "syncing".to_string(), 65 | current_item: table.to_string(), 66 | processed, 67 | total, 68 | status: "in_progress".to_string(), 69 | error: None, 70 | } 71 | } 72 | 73 | #[allow(dead_code)] 74 | pub fn database_completed(total: u32) -> Self { 75 | Self { 76 | sync_type: "database".to_string(), 77 | operation: "completed".to_string(), 78 | current_item: String::new(), 79 | processed: total, 80 | total, 81 | status: "completed".to_string(), 82 | error: None, 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/composables/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch, onUnmounted, type Ref, type WatchStopHandle } from "vue"; 2 | 3 | interface UseDebounceOptions { 4 | /** Delay in milliseconds (default: 300) */ 5 | delay?: number; 6 | /** Callback to execute on immediate (first) change */ 7 | immediate?: boolean; 8 | } 9 | 10 | /** 11 | * Composable for debouncing reactive values 12 | * Useful for search inputs, resize handlers, etc. 13 | * 14 | * @example 15 | * ```ts 16 | * const searchQuery = ref(""); 17 | * const debouncedQuery = useDebounce(searchQuery, { delay: 500 }); 18 | * 19 | * watch(debouncedQuery, (value) => { 20 | * // This will only fire 500ms after user stops typing 21 | * performSearch(value); 22 | * }); 23 | * ``` 24 | */ 25 | export function useDebounce( 26 | source: Ref, 27 | options: UseDebounceOptions = {}, 28 | ): Ref { 29 | const { delay = 300, immediate = false } = options; 30 | 31 | const debouncedValue = ref(source.value) as Ref; 32 | let timeoutId: ReturnType | null = null; 33 | let watchStopHandle: WatchStopHandle | null = null; 34 | 35 | watchStopHandle = watch( 36 | source, 37 | (newValue) => { 38 | // Clear previous timeout 39 | if (timeoutId !== null) { 40 | clearTimeout(timeoutId); 41 | } 42 | 43 | // If immediate, update immediately on first change 44 | if (immediate && timeoutId === null) { 45 | debouncedValue.value = newValue; 46 | } 47 | 48 | // Set new timeout 49 | timeoutId = setTimeout(() => { 50 | debouncedValue.value = newValue; 51 | timeoutId = null; 52 | }, delay); 53 | }, 54 | { immediate: true }, 55 | ); 56 | 57 | // Cleanup on unmount 58 | onUnmounted(() => { 59 | if (timeoutId !== null) { 60 | clearTimeout(timeoutId); 61 | } 62 | if (watchStopHandle) { 63 | watchStopHandle(); 64 | } 65 | }); 66 | 67 | return debouncedValue; 68 | } 69 | 70 | /** 71 | * Composable for debouncing function calls 72 | * Returns a debounced version of the function 73 | * 74 | * @example 75 | * ```ts 76 | * const handleSearch = useDebounceFn((query: string) => { 77 | * performSearch(query); 78 | * }, 500); 79 | * 80 | * // Call it - will only execute 500ms after last call 81 | * handleSearch("test"); 82 | * ``` 83 | */ 84 | export function useDebounceFn any>( 85 | fn: T, 86 | delay: number = 300, 87 | ): (...args: Parameters) => void { 88 | let timeoutId: ReturnType | null = null; 89 | 90 | return (...args: Parameters) => { 91 | if (timeoutId !== null) { 92 | clearTimeout(timeoutId); 93 | } 94 | 95 | timeoutId = setTimeout(() => { 96 | fn(...args); 97 | timeoutId = null; 98 | }, delay); 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /src/components/terminal-profiles/TerminalProfileItem.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 96 | -------------------------------------------------------------------------------- /src/components/sftp/CreateFileModal.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 102 | -------------------------------------------------------------------------------- /src/components/ui/EnvVarEditor.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 113 | -------------------------------------------------------------------------------- /src/services/savedCommand.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./api"; 2 | import type { 3 | SavedCommand, 4 | SavedCommandGroup, 5 | CreateSavedCommandRequest, 6 | UpdateSavedCommandRequest, 7 | CreateSavedCommandGroupRequest, 8 | UpdateSavedCommandGroupRequest, 9 | } from "../types/savedCommand"; 10 | 11 | /** 12 | * Saved Command service for frontend 13 | */ 14 | export const savedCommandService = { 15 | /** 16 | * Create new saved command 17 | */ 18 | async createCommand( 19 | request: CreateSavedCommandRequest, 20 | ): Promise { 21 | return await api.call("create_saved_command", request); 22 | }, 23 | 24 | /** 25 | * Get all saved commands 26 | */ 27 | async getCommands(): Promise { 28 | return await api.callRaw("get_saved_commands"); 29 | }, 30 | 31 | /** 32 | * Get saved command by ID 33 | */ 34 | async getCommand(id: string): Promise { 35 | return await api.callRaw("get_saved_command", id); 36 | }, 37 | 38 | /** 39 | * Update saved command 40 | */ 41 | async updateCommand( 42 | id: string, 43 | request: UpdateSavedCommandRequest, 44 | ): Promise { 45 | return await api.callRaw("update_saved_command", id, request); 46 | }, 47 | 48 | /** 49 | * Delete saved command 50 | */ 51 | async deleteCommand(id: string): Promise { 52 | return await api.callRaw("delete_saved_command", id); 53 | }, 54 | 55 | /** 56 | * Increment command usage count 57 | */ 58 | async incrementUsage(id: string): Promise { 59 | return await api.callRaw("increment_command_usage", id); 60 | }, 61 | 62 | /** 63 | * Toggle command favorite status 64 | */ 65 | async toggleFavorite(id: string): Promise { 66 | return await api.callRaw("toggle_command_favorite", id); 67 | }, 68 | 69 | /** 70 | * Create new saved command group 71 | */ 72 | async createGroup( 73 | request: CreateSavedCommandGroupRequest, 74 | ): Promise { 75 | return await api.call("create_saved_command_group", request); 76 | }, 77 | 78 | /** 79 | * Get all saved command groups 80 | */ 81 | async getGroups(): Promise { 82 | return await api.callRaw("get_saved_command_groups"); 83 | }, 84 | 85 | /** 86 | * Get saved command group by ID 87 | */ 88 | async getGroup(id: string): Promise { 89 | return await api.callRaw("get_saved_command_group", id); 90 | }, 91 | 92 | /** 93 | * Update saved command group 94 | */ 95 | async updateGroup( 96 | id: string, 97 | request: UpdateSavedCommandGroupRequest, 98 | ): Promise { 99 | return await api.callRaw("update_saved_command_group", id, request); 100 | }, 101 | 102 | /** 103 | * Delete saved command group 104 | */ 105 | async deleteGroup(id: string): Promise { 106 | return await api.callRaw("delete_saved_command_group", id); 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /src/components/sftp/CreateDirectoryModal.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 101 | -------------------------------------------------------------------------------- /src-tauri/src/models/sftp/sync.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::models::sftp::file_entry::FileEntry; 4 | 5 | /// Synchronization operation parameters 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | #[serde(rename_all = "camelCase")] 8 | pub struct SyncOperation { 9 | /// Synchronization direction 10 | pub direction: SyncDirection, 11 | /// Local directory path 12 | pub local_path: String, 13 | /// Remote directory path 14 | pub remote_path: String, 15 | /// Whether to delete files that exist in target but not in source 16 | pub delete_extra_files: bool, 17 | /// Whether to preserve symlinks 18 | pub preserve_symlinks: bool, 19 | /// Whether to preserve permissions 20 | pub preserve_permissions: bool, 21 | /// Maximum file size to sync (None = no limit) 22 | pub max_file_size: Option, 23 | /// File patterns to exclude (glob patterns) 24 | pub exclude_patterns: Vec, 25 | /// Clock skew tolerance in seconds (default: 1) 26 | /// Used when comparing file modification times between local and remote 27 | #[serde(default)] 28 | pub clock_skew_seconds: Option, 29 | /// Whether to verify file integrity using checksums 30 | /// When enabled, files with same size/time will be verified by checksum 31 | #[serde(default)] 32 | pub verify_checksum: bool, 33 | /// Checksum algorithm to use: "md5", "sha256" (default: "md5") 34 | #[serde(default)] 35 | pub checksum_algorithm: Option, 36 | } 37 | 38 | /// Synchronization direction 39 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 40 | #[serde(rename_all = "lowercase")] 41 | pub enum SyncDirection { 42 | /// Sync from local to remote (upload) 43 | LocalToRemote, 44 | /// Sync from remote to local (download) 45 | RemoteToLocal, 46 | /// Bidirectional sync (merge both ways) 47 | Bidirectional, 48 | } 49 | 50 | /// Represents a difference between local and remote files 51 | #[derive(Debug, Clone, Serialize, Deserialize)] 52 | #[serde(rename_all = "camelCase")] 53 | pub struct DiffEntry { 54 | /// File path relative to sync root 55 | pub path: String, 56 | /// Difference type 57 | pub diff_type: DiffType, 58 | /// Local file entry (if exists) 59 | pub local_entry: Option, 60 | /// Remote file entry (if exists) 61 | pub remote_entry: Option, 62 | } 63 | 64 | /// Type of difference between local and remote 65 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] 66 | #[serde(rename_all = "camelCase")] 67 | pub enum DiffType { 68 | /// File exists only on local 69 | OnlyLocal, 70 | /// File exists only on remote 71 | OnlyRemote, 72 | /// File exists on both but sizes differ 73 | SizeDiffers, 74 | /// File exists on both but modification times differ 75 | TimeDiffers, 76 | /// File exists on both and appears identical 77 | Identical, 78 | /// File exists on both but permissions differ 79 | PermissionsDiffer, 80 | /// File exists on both but checksums differ 81 | ChecksumDiffers, 82 | } 83 | -------------------------------------------------------------------------------- /src-tauri/src/core/title_detector.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | 3 | /// Detects terminal title from various sources 4 | pub struct TitleDetector { 5 | /// Regex to match ANSI title escape sequences 6 | ansi_title_regex: Regex, 7 | /// Regex to match user@hostname from prompts 8 | user_host_regex: Regex, 9 | /// Last detected title 10 | last_title: Option, 11 | } 12 | 13 | impl TitleDetector { 14 | pub fn new() -> Self { 15 | let ansi_title_regex = Regex::new(r"\x1b\](?:0|2);([^\x07\x1b]*?)(?:\x07|\x1b\\)").unwrap(); 16 | 17 | let user_host_regex = Regex::new(r"\b([a-zA-Z0-9_-]+)@([a-zA-Z0-9._-]+)\b").unwrap(); 18 | 19 | Self { 20 | ansi_title_regex, 21 | user_host_regex, 22 | last_title: None, 23 | } 24 | } 25 | 26 | /// Process terminal output and detect title changes 27 | pub fn process_output(&mut self, data: &[u8]) -> Option { 28 | let text = String::from_utf8_lossy(data); 29 | 30 | if let Some(ansi_title) = self.extract_title_from_ansi(&text) { 31 | if let Some(user_host) = self.extract_user_host_from_text(&ansi_title) { 32 | if Some(&user_host) != self.last_title.as_ref() { 33 | self.last_title = Some(user_host.clone()); 34 | return Some(user_host); 35 | } 36 | } 37 | } 38 | 39 | None 40 | } 41 | 42 | /// Extract title from ANSI escape sequences 43 | fn extract_title_from_ansi(&self, text: &str) -> Option { 44 | if let Some(captures) = self.ansi_title_regex.captures(text) { 45 | if let Some(title_match) = captures.get(1) { 46 | let title = title_match.as_str().trim(); 47 | if !title.is_empty() { 48 | return Some(title.to_string()); 49 | } 50 | } 51 | } 52 | None 53 | } 54 | 55 | /// Extract user@hostname from text 56 | fn extract_user_host_from_text(&self, text: &str) -> Option { 57 | if let Some(captures) = self.user_host_regex.captures(text) { 58 | if let (Some(user), Some(host)) = (captures.get(1), captures.get(2)) { 59 | let user_str = user.as_str(); 60 | let host_str = host.as_str(); 61 | 62 | if user_str.is_empty() 63 | || host_str.is_empty() 64 | || user_str.len() > 50 65 | || host_str.len() > 50 66 | { 67 | return None; 68 | } 69 | 70 | if user_str.len() < 2 { 71 | return None; 72 | } 73 | 74 | if host_str.chars().all(|c| c.is_ascii_digit() || c == '.') { 75 | return None; 76 | } 77 | 78 | return Some(format!("{}@{}", user_str, host_str)); 79 | } 80 | } 81 | 82 | None 83 | } 84 | } 85 | 86 | impl Default for TitleDetector { 87 | fn default() -> Self { 88 | Self::new() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/components/sftp/FileRenameModal.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 105 | -------------------------------------------------------------------------------- /src/components/history/HistoryItem.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 82 | 83 | 100 | -------------------------------------------------------------------------------- /src/components/ui/SkeletonLoader.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 101 | 102 | 126 | -------------------------------------------------------------------------------- /src/services/auth.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./api"; 2 | import type { 3 | MasterPasswordSetup, 4 | MasterPasswordVerification, 5 | MasterPasswordStatus, 6 | MasterPasswordChange, 7 | MasterPasswordConfig, 8 | } from "../types/auth"; 9 | 10 | /** 11 | * Setup master password for the first time 12 | * @param setup - Master password setup configuration 13 | */ 14 | export async function setup(setup: MasterPasswordSetup): Promise { 15 | return await api.call("setup_master_password", { 16 | ...setup, 17 | autoLockTimeout: Number(setup.autoLockTimeout), 18 | useKeychain: setup.useKeychain || false, 19 | }); 20 | } 21 | 22 | /** 23 | * Verify master password 24 | * @param verification - Master password verification request 25 | * @returns True if password is valid, false otherwise 26 | */ 27 | export async function verify( 28 | verification: MasterPasswordVerification, 29 | ): Promise { 30 | return await api.call("verify_master_password", verification); 31 | } 32 | 33 | /** 34 | * Try to auto-unlock using keychain 35 | * @returns True if auto-unlock was successful, false otherwise 36 | */ 37 | export async function tryAutoUnlock(): Promise { 38 | return await api.callRaw("try_auto_unlock"); 39 | } 40 | 41 | /** 42 | * Lock the current session 43 | */ 44 | export async function lock(): Promise { 45 | return await api.callRaw("lock_session"); 46 | } 47 | 48 | /** 49 | * Change master password 50 | * @param change - Master password change request 51 | */ 52 | export async function change(change: MasterPasswordChange): Promise { 53 | return await api.call("change_master_password", change); 54 | } 55 | 56 | /** 57 | * Get master password status 58 | * @returns Current master password status 59 | */ 60 | export async function getStatus(): Promise { 61 | return await api.callRaw("get_master_password_status"); 62 | } 63 | 64 | /** 65 | * Reset master password (removes all encrypted data) 66 | */ 67 | export async function reset(): Promise { 68 | return await api.callRaw("reset_master_password"); 69 | } 70 | 71 | /** 72 | * Update master password configuration 73 | * @param config - New configuration 74 | */ 75 | export async function updateConfig( 76 | config: Partial, 77 | ): Promise { 78 | return await api.call("update_master_password_config", config); 79 | } 80 | 81 | /** 82 | * Get current device information 83 | * @returns Current device information 84 | */ 85 | export async function getCurrentDevice(): Promise { 86 | return await api.callRaw("get_current_device"); 87 | } 88 | 89 | /** 90 | * Check if session is valid (not expired) 91 | * @returns True if session is valid, false otherwise 92 | */ 93 | export async function isSessionValid(): Promise { 94 | return await api.callRaw("is_session_valid"); 95 | } 96 | 97 | /** 98 | * Get master password configuration 99 | * @returns Master password configuration 100 | */ 101 | export async function getConfig(): Promise { 102 | return await api.callRaw("get_master_password_config"); 103 | } 104 | -------------------------------------------------------------------------------- /scripts/create-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ------------------------------------------ 3 | # Script to update version and recrete tag to trigger GitHub Actions 4 | # Written by klpod221 - github.com/klpod221 5 | # ------------------------------------------ 6 | 7 | set -e 8 | 9 | SEPARATOR="==========================================" 10 | 11 | if [[ -z "$1" ]]; then 12 | echo "Usage: $0 " 13 | echo "Example: $0 v2.1.3" 14 | exit 1 15 | fi 16 | 17 | TAG="$1" 18 | 19 | # Validate tag format 20 | if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then 21 | echo "⚠️ Warning: Tag should follow format vX.Y.Z (e.g., v2.1.3)" 22 | read -p "Continue anyway? (y/n): " CONTINUE 23 | if [[ "$CONTINUE" != "y" ]]; then 24 | exit 0 25 | fi 26 | fi 27 | 28 | echo "$SEPARATOR" 29 | echo " Update Version to match Tag: $TAG" 30 | echo "$SEPARATOR" 31 | echo "" 32 | 33 | VERSION="${TAG:1}" # Remove the 'v' prefix 34 | echo "🔄 Updating version to $VERSION in relevant files..." 35 | # Update version in package.json 36 | sed -i.bak -E "s/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$VERSION\"/" package.json 37 | 38 | # Update version in src-tauri/Cargo.toml 39 | sed -i.bak -E "s/^version = \"[0-9]+\.[0-9]+\.[0-9]+\"/version = \"$VERSION\"/" src-tauri/Cargo.toml 40 | 41 | # update version in src-tauri/tauri.conf.json 42 | sed -i.bak -E "s/\"version\": \"[0-9]+\.[0-9]+\.[0-9]+\"/\"version\": \"$VERSION\"/" src-tauri/tauri.conf.json 43 | 44 | # Clean up backup files 45 | rm package.json.bak src-tauri/Cargo.toml.bak src-tauri/tauri.conf.json.bak 46 | 47 | # Update lock files 48 | npm install --package-lock-only 49 | cd src-tauri && cargo check && cd .. 50 | 51 | echo "$SEPARATOR" 52 | echo " Committing Version Update" 53 | echo "$SEPARATOR" 54 | echo "" 55 | 56 | git add . 57 | 58 | # Check if there are any changes to commit 59 | if git diff --cached --quiet && git diff --quiet; then 60 | echo "ℹ️ No changes to commit. Skipping commit and push." 61 | else 62 | echo "📝 Committing changes..." 63 | git commit -m "chore: update version to $VERSION" 64 | echo "📤 Pushing to origin main..." 65 | git push origin main > /dev/null 66 | echo "✅ Changes committed and pushed successfully." 67 | fi 68 | 69 | echo "$SEPARATOR" 70 | echo " Recreating Git Tag: $TAG" 71 | echo "$SEPARATOR" 72 | echo "" 73 | 74 | # Check if tag exists locally 75 | if git rev-parse "$TAG" >/dev/null 2>&1; then 76 | echo "🗑️ Deleting local tag: $TAG" 77 | git tag -d "$TAG" 78 | else 79 | echo "ℹ️ Local tag doesn't exist" 80 | fi 81 | 82 | # Check if tag exists on remote 83 | if git ls-remote --tags origin | grep -q "refs/tags/$TAG"; then 84 | echo "🗑️ Deleting remote tag: $TAG" 85 | git push origin ":refs/tags/$TAG" 86 | else 87 | echo "ℹ️ Remote tag doesn't exist" 88 | fi 89 | 90 | echo "" 91 | echo "✨ Creating new tag: $TAG" 92 | git tag "$TAG" 93 | 94 | echo "📤 Pushing tag to remote..." 95 | git push origin "$TAG" 96 | 97 | echo "" 98 | echo "✅ Done! Version updated and tag recreated." 99 | echo "" 100 | echo "🚀 GitHub Actions workflow should start running soon." 101 | echo "" 102 | echo "View the workflow at:" 103 | echo "https://github.com/klpod221/kerminal/actions" 104 | -------------------------------------------------------------------------------- /src-tauri/src/models/base.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use serde::{Deserialize, Serialize}; 3 | use sha2::{Digest, Sha256}; 4 | use uuid::Uuid; 5 | 6 | use crate::database::traits::SyncStatus; 7 | 8 | /// Base model that provides common fields for all syncable models 9 | #[derive(Debug, Clone, Serialize, Deserialize)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct BaseModel { 12 | pub id: String, 13 | pub created_at: DateTime, 14 | pub updated_at: DateTime, 15 | pub device_id: String, 16 | pub version: u64, 17 | pub sync_status: SyncStatus, 18 | } 19 | 20 | impl BaseModel { 21 | /// Create a new base model with current timestamp and device ID 22 | pub fn new(device_id: String) -> Self { 23 | let now = Utc::now(); 24 | Self { 25 | id: Uuid::new_v4().to_string(), 26 | created_at: now, 27 | updated_at: now, 28 | device_id, 29 | version: 1, 30 | sync_status: SyncStatus::Pending, 31 | } 32 | } 33 | 34 | /// Update the timestamp and increment version 35 | pub fn touch(&mut self) { 36 | self.updated_at = Utc::now(); 37 | self.version += 1; 38 | self.sync_status = SyncStatus::Pending; 39 | } 40 | 41 | #[allow(dead_code)] 42 | pub fn generate_checksum(&self, model: &T) -> String { 43 | let json = serde_json::to_string(model).unwrap_or_default(); 44 | let mut hasher = Sha256::new(); 45 | hasher.update(json.as_bytes()); 46 | format!("{:x}", hasher.finalize()) 47 | } 48 | } 49 | 50 | impl Default for BaseModel { 51 | fn default() -> Self { 52 | Self::new("unknown".to_string()) 53 | } 54 | } 55 | 56 | /// Macro to automatically implement Syncable trait for models with BaseModel 57 | #[macro_export] 58 | macro_rules! impl_syncable { 59 | ($model:ty, $table:expr) => { 60 | impl $crate::database::traits::Syncable for $model { 61 | fn table_name() -> &'static str { 62 | $table 63 | } 64 | 65 | fn id(&self) -> &str { 66 | &self.base.id 67 | } 68 | 69 | fn device_id(&self) -> &str { 70 | &self.base.device_id 71 | } 72 | 73 | fn created_at(&self) -> chrono::DateTime { 74 | self.base.created_at 75 | } 76 | 77 | fn updated_at(&self) -> chrono::DateTime { 78 | self.base.updated_at 79 | } 80 | 81 | fn version(&self) -> u64 { 82 | self.base.version 83 | } 84 | 85 | fn set_version(&mut self, version: u64) { 86 | self.base.version = version; 87 | } 88 | 89 | fn sync_status(&self) -> &$crate::database::traits::SyncStatus { 90 | &self.base.sync_status 91 | } 92 | 93 | fn set_sync_status(&mut self, status: $crate::database::traits::SyncStatus) { 94 | self.base.sync_status = status; 95 | } 96 | 97 | fn checksum(&self) -> String { 98 | self.base.generate_checksum(self) 99 | } 100 | } 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /src/components/ui/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 113 | --------------------------------------------------------------------------------