DEV Community

Cover image for How I Built Kerminal: A Free, Open-Source Terminal & SSH Manager with Multi-Device Sync
klpod221
klpod221

Posted on

How I Built Kerminal: A Free, Open-Source Terminal & SSH Manager with Multi-Device Sync

As developers, we spend countless hours in terminal emulators. Whether we're SSHing into remote servers, running build scripts, or managing deployments, the terminal is our second home. But most terminal applications feel outdated, lack modern features, and don't prioritize security.

That's why I built Kerminal—a modern, feature-rich terminal emulator with advanced SSH management, multi-device synchronization, and enterprise-grade encryption. In this article, I'll share the journey, the technical decisions, and the lessons learned.

Why Build Another Terminal Emulator?

The story of Kerminal started with a simple, urgent need: I needed a terminal app that could manage my SSH connections and sync them across devices.

Like many developers, I was juggling multiple servers and constantly switching between my work laptop, personal machine, and sometimes even my home server. Managing SSH connections manually was becoming a nightmare—I'd set up profiles on one device, forget the exact configuration, and have to recreate them on another.

So I did what any developer would do: I looked for existing solutions.

The Search for the Perfect Terminal App

I tried Termius first. It looked promising—beautiful UI, great sync functionality, and all the features I needed. But there was a catch: the sync feature required a paid subscription. As someone who values open-source solutions and prefers not to pay for tools I use daily, this was a deal-breaker.

Next, I tried Tabby (formerly Terminus). It's open-source, has a modern interface, and I really liked the design. However, it lacked the multi-device synchronization feature I desperately needed. I could manage connections locally, but switching devices meant starting from scratch.

The "Scratch Your Own Itch" Moment

Faced with these limitations, I had two options:

  1. Pay for Termius (not ideal)
  2. Keep using Tabby and manually sync configs (tedious)
  3. Build my own solution 🚀

I chose option 3. After all, I'm a developer—building tools is what I do! Plus, this would be a great learning opportunity to work with modern desktop app frameworks.

The Hasty First Version: Electron

Since I needed this app now (not in 6 months), I made a pragmatic decision: I'd build the first version quickly using Electron. I was already familiar with web technologies, and Electron would let me ship something usable fast.

The first version was functional but had the typical Electron drawbacks:

  • Large bundle size: ~100MB+
  • Slower startup time: Not ideal for a tool I'd use constantly
  • Poor performance: When terminal output is large, or using multiple terminal tabs, the app would freeze for a few seconds and take a lot of memory

But it worked! I could manage SSH profiles, sync them to a database, and use it daily. Mission accomplished... for now.

The Migration to Tauri

Once I had a working solution and wasn't under time pressure, I started thinking about the long-term. I'd heard about Tauri—a framework that promised Electron-like capabilities but with Rust backend and much better performance.

The migration wasn't trivial, but it was worth it:

Before (Electron):

  • Bundle size: ~120MB
  • Memory usage: ~200-300MB idle
  • Startup: 2-3 seconds

After (Tauri):

  • Bundle size: ~15MB (8x smaller!)
  • Memory usage: ~50-80MB idle (4x less!)
  • Startup: <1 second (3x faster!)

The performance improvements were immediately noticeable, especially when opening multiple terminal tabs or working with large file transfers via SFTP.

What I Learned

This journey taught me an important lesson: Sometimes the "quick and dirty" solution is the right first step. Building with Electron first let me:

  1. Validate the idea quickly
  2. Get user feedback early
  3. Understand the requirements better
  4. Then optimize with the right technology later

Perfect is the enemy of good, and shipping something usable is better than never shipping at all.

Now, Kerminal combines the best of both worlds: the rapid development experience of web technologies (Vue 3) with the performance and security of native code (Rust + Tauri).

The Tech Stack

Frontend: Vue 3 + TypeScript

I chose Vue 3 for its excellent developer experience and performance:

// Example: Terminal component with Composition API
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Terminal } from 'xterm'
import { FitAddon } from '@xterm/addon-fit'

const terminalRef = ref<HTMLElement>()
let terminal: Terminal

onMounted(() => {
  terminal = new Terminal({
    theme: { background: '#1e1e1e' },
    fontFamily: 'Fira Code',
    fontSize: 14
  })

  const fitAddon = new FitAddon()
  terminal.loadAddon(fitAddon)
  terminal.open(terminalRef.value!)
  fitAddon.fit()
})
</script>
Enter fullscreen mode Exit fullscreen mode

Why Vue 3?

  • Composition API for better code organization
  • Excellent TypeScript support
  • Small bundle size
  • Reactive system that works seamlessly with terminal libraries
  • I'm already familiar with Vue 3 and TypeScript, so it was a natural choice

Backend: Rust + Tauri v2

Tauri v2 was the perfect choice for building a secure, performant desktop app:

// Example: Secure SSH key storage
use tauri::command;
use aes_gcm::{Aes256Gcm, KeyInit, aead::Aead};

#[command]
async fn encrypt_ssh_key(key: String, master_password: String) -> Result<String, String> {
    let cipher = Aes256Gcm::new_from_slice(
        &derive_key(master_password)?
    );

    let nonce = generate_nonce();
    let ciphertext = cipher.encrypt(&nonce, key.as_bytes())
        .map_err(|e| format!("Encryption failed: {}", e))?;

    Ok(base64::encode(ciphertext))
}
Enter fullscreen mode Exit fullscreen mode

Why Tauri?

  • Security: Smaller attack surface than Electron
  • Performance: Native Rust backend with minimal overhead
  • Bundle size: ~10MB vs Electron's ~100MB+
  • Memory: Significantly lower memory footprint
  • Native APIs: Direct access to system features
  • The biggest reason is that I really want to learn Rust and Tauri to be able to continue building the tools I need for myself

Terminal Rendering: xterm.js with WebGL

For terminal emulation, I used xterm.js with WebGL acceleration:

// Example Code
import { Terminal } from 'xterm'
import { WebglAddon } from '@xterm/addon-webgl'
import { Unicode11Addon } from '@xterm/addon-unicode11'

const terminal = new Terminal({
  rendererType: 'dom', // Can switch to 'canvas' or 'webgl'
  allowProposedApi: true
})

// Enable WebGL for better performance
terminal.loadAddon(new WebglAddon())
terminal.loadAddon(new Unicode11Addon())
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Hardware-accelerated rendering
  • Unicode 11 support for emojis and special characters
  • Smooth scrolling even with large outputs
  • Clickable links and search functionality

Key Features & Implementation

1. SSH Profile Management with Encryption

One of Kerminal's core features is secure SSH profile storage. All sensitive data is encrypted using AES-256-GCM:

// Simplified encryption flow
pub struct EncryptionService {
    master_key: Vec<u8>,
}

impl EncryptionService {
    pub fn encrypt(&self, plaintext: &str) -> Result<EncryptedData, Error> {
        let cipher = Aes256Gcm::new_from_slice(&self.master_key)?;
        let nonce = generate_nonce();
        let ciphertext = cipher.encrypt(&nonce, plaintext.as_bytes())?;

        Ok(EncryptedData {
            ciphertext: base64::encode(ciphertext),
            nonce: base64::encode(nonce),
        })
    }

    pub fn decrypt(&self, encrypted: &EncryptedData) -> Result<String, Error> {
        let cipher = Aes256Gcm::new_from_slice(&self.master_key)?;
        let nonce = base64::decode(&encrypted.nonce)?;
        let ciphertext = base64::decode(&encrypted.ciphertext)?;

        let plaintext = cipher.decrypt(&nonce.into(), ciphertext.as_ref())?;
        Ok(String::from_utf8(plaintext)?)
    }
}
Enter fullscreen mode Exit fullscreen mode

Security Features:

  • Master password never stored (only verification hash)
  • Device-specific encryption keys
  • Automatic session locking after inactivity
  • Platform keychain integration

2. Multi-Device Synchronization

Kerminal supports syncing SSH profiles, saved commands, and settings across devices using MySQL, PostgreSQL, or MongoDB:

// Sync service implementation
class SyncService {
  async syncProfiles(): Promise<void> {
    const localProfiles = await getLocalProfiles()
    const remoteProfiles = await this.fetchRemoteProfiles()

    // Conflict resolution
    const conflicts = this.detectConflicts(localProfiles, remoteProfiles)

    if (conflicts.length > 0) {
      await this.resolveConflicts(conflicts)
    }

    // Merge changes
    const merged = this.mergeProfiles(localProfiles, remoteProfiles)
    await this.saveProfiles(merged)
    await this.uploadProfiles(merged)
  }
}
Enter fullscreen mode Exit fullscreen mode

Sync Features:

  • Encrypted data transmission
  • Conflict resolution strategies (last-write-wins, manual merge)
  • Device management and tracking
  • Auto-sync on changes

3. SFTP File Browser

I built a full-featured SFTP browser with file preview, transfer queue, and batch operations:

<template>
  <div class="sftp-browser">
    <FileBrowser
      :files="files"
      @select="handleFileSelect"
      @upload="handleUpload"
      @download="handleDownload"
    />
    <TransferQueue
      :transfers="activeTransfers"
      @pause="pauseTransfer"
      @resume="resumeTransfer"
    />
    <FilePreviewModal
      v-if="selectedFile"
      :file="selectedFile"
      @close="selectedFile = null"
    />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Features:

  • Drag-and-drop file transfers
  • Image preview
  • Batch operations (select, delete, download)
  • Progress indicators
  • File Editor

4. Session Recording

Kerminal records terminal sessions in asciicast v2 format:

class SessionRecorder {
  private recording: AsciicastRecording = {
    version: 2,
    width: 80,
    height: 24,
    timestamp: Date.now(),
    events: []
  }

  recordEvent(type: 'o' | 'i', data: string, time: number) {
    this.recording.events.push([time, type, data])
  }

  async save(filename: string) {
    const json = JSON.stringify(this.recording, null, 2)
    await writeFile(filename, json)
  }
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Share terminal sessions with team
  • Document complex workflows
  • Debug issues by replaying sessions
  • Onboarding new team members

Architecture Decisions

State Management: Pinia

I used Pinia for centralized state management:

// stores/sshProfiles.ts
export const useSSHProfilesStore = defineStore('sshProfiles', {
  state: () => ({
    profiles: [] as SSHProfile[],
    selectedProfile: null as SSHProfile | null,
  }),

  actions: {
    async loadProfiles() {
      const encrypted = await invoke('get_encrypted_profiles')
      this.profiles = await decryptProfiles(encrypted)
    },

    async saveProfile(profile: SSHProfile) {
      const encrypted = await encryptProfile(profile)
      await invoke('save_profile', { profile: encrypted })
      await this.loadProfiles()
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Tauri Commands for Backend Communication

All sensitive operations go through Tauri commands:

#[tauri::command]
async fn connect_ssh(
    profile_id: String,
    password: Option<String>,
    app_handle: AppHandle,
) -> Result<String, String> {
    let profile = get_profile(&profile_id).await?;
    let decrypted_key = decrypt_ssh_key(&profile.private_key).await?;

    let mut session = russh::client::connect(
        Config::default(),
        (profile.host.as_str(), profile.port),
        ClientSession::new(decrypted_key),
    ).await?;

    // Store session in app state
    app_handle.manage(SessionStore::new(session));

    Ok("Connected".to_string())
}
Enter fullscreen mode Exit fullscreen mode

Challenges & Solutions

Challenge 1: SSH Key Security

Problem: Storing SSH private keys securely without compromising usability.

Solution:

  • Encrypt keys with AES-256-GCM using a master password
  • Derive encryption key using Argon2 (memory-hard KDF)
  • Never store master password, only verification hash
  • Use platform keychain for auto-unlock

Challenge 2: Cross-Platform Compatibility

Problem: Different SSH behaviors on Windows, macOS, and Linux.

Solution:

  • Use russh Rust library for consistent SSH implementation
  • Platform-specific code only for UI and keychain access
  • Comprehensive testing on all platforms

Challenge 3: Terminal Performance

Problem: Rendering large terminal outputs caused lag.

Solution:

  • WebGL renderer for hardware acceleration
  • Virtual scrolling for terminal buffer
  • Limit buffer size with configurable history
  • Optimize rendering with requestAnimationFrame

Lessons Learned

  1. Security First: Building security into the architecture from day one is easier than retrofitting it later.

  2. Tauri is Powerful: The combination of Rust backend and web frontend gives you the best of both worlds—performance and developer experience.

  3. TypeScript is Essential: Strong typing caught many bugs early and made refactoring safer.

  4. User Feedback Matters: Early beta testers provided invaluable feedback that shaped the product.

  5. Documentation is Key: Good documentation (both code and user docs) saves time in the long run.

What's Next?

Kerminal is actively developed with exciting features on the roadmap:

  • Plugin system for extensions
  • Cloud backup integration
  • Web-based version (PWA)
  • Mobile app companion
  • Enhanced accessibility features

Try Kerminal

Kerminal is open source and available on GitHub:

Conclusion

Building Kerminal has been an incredible learning experience. Combining modern web technologies (Vue 3) with native performance (Rust + Tauri) allowed me to create a terminal emulator that's both powerful and secure.

If you're considering building a desktop application, I highly recommend exploring Tauri. It offers a great balance of performance, security, and developer experience.

What terminal features would you like to see in a modern terminal emulator? Share your thoughts in the comments!


If you found this article helpful, consider giving Kerminal a star on GitHub or trying it out. Your feedback and contributions are always welcome!

Top comments (0)