├── dist ├── assets │ └── favicon.svg ├── stores.d.ts ├── index.d.ts ├── index.js ├── stores.js ├── utils.d.ts ├── types.js ├── utils.js ├── shell.d.ts ├── Terminal.svelte.d.ts ├── types.d.ts ├── shell.js └── Terminal.svelte ├── .npmrc ├── static ├── robots.txt └── svelte-bash.jpg ├── .vscode └── settings.json ├── src ├── routes │ ├── layout.css │ ├── +layout.svelte │ └── +page.svelte ├── lib │ ├── index.ts │ ├── stores.ts │ ├── utils.ts │ ├── types.ts │ ├── shell.ts │ └── Terminal.svelte ├── app.d.ts └── app.html ├── vite.config.ts ├── .gitignore ├── svelte.config.js ├── tsconfig.json ├── package.json └── README.md /dist/assets/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /dist/stores.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | # allow crawling everything by default 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.css": "tailwindcss" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /static/svelte-bash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YusufCeng1z/svelte-bash/HEAD/static/svelte-bash.jpg -------------------------------------------------------------------------------- /src/routes/layout.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @plugin '@tailwindcss/forms'; 3 | @plugin '@tailwindcss/typography'; 4 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as Terminal } from './Terminal.svelte'; 2 | export * from './types'; 3 | export { Shell } from './shell'; 4 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | export { default as Terminal } from './Terminal.svelte'; 2 | export * from './types'; 3 | export { Shell } from './shell'; 4 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export { default as Terminal } from './Terminal.svelte'; 3 | export * from './types'; 4 | export { Shell } from './shell'; -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {@render children()} 7 | -------------------------------------------------------------------------------- /dist/stores.js: -------------------------------------------------------------------------------- 1 | // We are moving state to local component scope to support multiple instances. 2 | // This file is now deprecated or can be used for shared utilities if needed. 3 | // For now, exporting nothing to avoid confusion. 4 | export {}; 5 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import tailwindcss from '@tailwindcss/vite'; 2 | import { sveltekit } from '@sveltejs/kit/vite'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [tailwindcss(), sveltekit()] 7 | }); 8 | -------------------------------------------------------------------------------- /dist/utils.d.ts: -------------------------------------------------------------------------------- 1 | import type { FileStructure } from './types'; 2 | export declare function resolvePath(structure: FileStructure, cwdParts: string[], targetPath: string): { 3 | node: string | FileStructure | undefined; 4 | name: string; 5 | }; 6 | -------------------------------------------------------------------------------- /src/lib/stores.ts: -------------------------------------------------------------------------------- 1 | 2 | // We are moving state to local component scope to support multiple instances. 3 | // This file is now deprecated or can be used for shared utilities if needed. 4 | // For now, exporting nothing to avoid confusion. 5 | export { }; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %sveltekit.head% 7 | 8 | 9 |
%sveltekit.body%
10 | 11 | 12 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter() 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /dist/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Predefined themes available in the library. 3 | */ 4 | export const Themes = { 5 | dark: { 6 | background: '#1a1b26', 7 | foreground: '#a9b1d6', 8 | prompt: '#7aa2f7', 9 | cursor: '#c0caf5' 10 | }, 11 | light: { 12 | background: '#ffffff', 13 | foreground: '#000000', 14 | prompt: '#2563eb', 15 | cursor: '#000000' 16 | }, 17 | dracula: { 18 | background: '#282a36', 19 | foreground: '#f8f8f2', 20 | prompt: '#bd93f9', 21 | cursor: '#f8f8f2' 22 | }, 23 | matrix: { 24 | background: '#000000', 25 | foreground: '#00ff00', 26 | prompt: '#00cc00', 27 | cursor: '#00ff00' 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | export function resolvePath(structure, cwdParts, targetPath) { 2 | if (targetPath === '/') { 3 | return { node: structure, name: '' }; 4 | } 5 | const parts = targetPath.split('/'); 6 | let currentParts = [...cwdParts]; 7 | let pointer = structure; 8 | // Navigate to CWD first 9 | for (const p of currentParts) { 10 | if (pointer[p] && typeof pointer[p] === 'object') { 11 | pointer = pointer[p]; 12 | } 13 | } 14 | if (pointer[targetPath]) { 15 | return { node: pointer[targetPath], name: targetPath }; 16 | } 17 | if (targetPath === '..') { 18 | return { node: undefined, name: targetPath }; 19 | } 20 | return { node: undefined, name: targetPath }; 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "rewriteRelativeImportExtensions": true, 5 | "allowJs": true, 6 | "checkJs": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "resolveJsonModule": true, 10 | "skipLibCheck": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "moduleResolution": "bundler" 14 | } 15 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 16 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 17 | // 18 | // To make changes to top-level options such as include and exclude, we recommend extending 19 | // the generated config; see https://svelte.dev/docs/kit/configuration#typescript 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { FileStructure } from './types'; 3 | 4 | export function resolvePath(structure: FileStructure, cwdParts: string[], targetPath: string): { node: string | FileStructure | undefined, name: string } { 5 | if (targetPath === '/') { 6 | return { node: structure, name: '' }; 7 | } 8 | 9 | const parts = targetPath.split('/'); 10 | let currentParts = [...cwdParts]; 11 | 12 | 13 | let pointer: any = structure; 14 | 15 | // Navigate to CWD first 16 | for (const p of currentParts) { 17 | if (pointer[p] && typeof pointer[p] === 'object') { 18 | pointer = pointer[p]; 19 | } 20 | } 21 | 22 | if (pointer[targetPath]) { 23 | return { node: pointer[targetPath], name: targetPath }; 24 | } 25 | 26 | if (targetPath === '..') { 27 | return { node: undefined, name: targetPath }; 28 | } 29 | 30 | return { node: undefined, name: targetPath }; 31 | } 32 | -------------------------------------------------------------------------------- /dist/shell.d.ts: -------------------------------------------------------------------------------- 1 | import type { FileStructure, CommandHandler, TerminalLine } from './types'; 2 | export declare class Shell { 3 | private structure; 4 | private customCommands; 5 | private cwd; 6 | private history; 7 | private user; 8 | constructor(structure?: FileStructure, commands?: Record, user?: string, history?: TerminalLine[]); 9 | get currentPath(): string[]; 10 | setHistory(h: TerminalLine[]): void; 11 | setStructure(s: FileStructure): void; 12 | setCommands(c: Record): void; 13 | /** 14 | * Resolve path parts to a node in the file structure. 15 | */ 16 | private resolveNode; 17 | /** 18 | * Process a command string and return the updated history lines (or void if direct history mutation). 19 | * Actually, let's return a list of NEW lines provided by the command execution. 20 | */ 21 | execute(input: string): Promise; 22 | } 23 | -------------------------------------------------------------------------------- /dist/Terminal.svelte.d.ts: -------------------------------------------------------------------------------- 1 | interface $$__sveltets_2_IsomorphicComponent = any, Events extends Record = any, Slots extends Record = any, Exports = {}, Bindings = string> { 2 | new (options: import('svelte').ComponentConstructorOptions): import('svelte').SvelteComponent & { 3 | $$bindings?: Bindings; 4 | } & Exports; 5 | (internal: unknown, props: Props & { 6 | $$events?: Events; 7 | $$slots?: Slots; 8 | }): Exports & { 9 | $set?: any; 10 | $on?: any; 11 | }; 12 | z_$$bindings?: Bindings; 13 | } 14 | declare const Terminal: $$__sveltets_2_IsomorphicComponent<{ 15 | [x: string]: any; 16 | commands?: Record | undefined; 17 | structure?: import("./types").FileStructure | undefined; 18 | theme?: string | import("./types").Theme | undefined; 19 | welcomeMessage?: string | string[] | undefined; 20 | user?: string | undefined; 21 | promptStr?: string | undefined; 22 | autoplay?: { 23 | command: string; 24 | output?: string | string[] | import("svelte").Component; 25 | }[] | undefined; 26 | autoplayLoop?: boolean | undefined; 27 | typingSpeed?: number | undefined; 28 | class?: string | undefined; 29 | }, { 30 | [evt: string]: CustomEvent; 31 | }, {}, {}, string>; 32 | type Terminal = InstanceType; 33 | export default Terminal; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-bash", 3 | "version": "1.0.2", 4 | "description": "A fully typed, customizable, and lightweight terminal component for Svelte 5.", 5 | "type": "module", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/YusufCeng1z/svelte-bash.git" 10 | }, 11 | "homepage": "https://svelte-bash.netlify.app", 12 | "bugs": { 13 | "url": "https://github.com/YusufCeng1z/svelte-bash/issues" 14 | }, 15 | "keywords": [ 16 | "svelte", 17 | "terminal", 18 | "console", 19 | "cli", 20 | "bash", 21 | "shell", 22 | "emulator", 23 | "component", 24 | "ui", 25 | "svelte-5", 26 | "runes" 27 | ], 28 | "exports": { 29 | ".": { 30 | "types": "./dist/index.d.ts", 31 | "svelte": "./dist/index.js" 32 | } 33 | }, 34 | "files": [ 35 | "dist", 36 | "!dist/**/*.test.*", 37 | "!dist/**/*.spec.*" 38 | ], 39 | "peerDependencies": { 40 | "svelte": "^5.0.0" 41 | }, 42 | "scripts": { 43 | "dev": "vite dev", 44 | "build": "vite build", 45 | "package": "svelte-package -o dist", 46 | "preview": "vite preview", 47 | "prepare": "svelte-kit sync || echo ''", 48 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 49 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 50 | }, 51 | "devDependencies": { 52 | "@sveltejs/adapter-auto": "^7.0.0", 53 | "@sveltejs/kit": "^2.48.5", 54 | "@sveltejs/package": "^2.0.0", 55 | "@sveltejs/vite-plugin-svelte": "^6.2.1", 56 | "@tailwindcss/forms": "^0.5.10", 57 | "@tailwindcss/typography": "^0.5.19", 58 | "@tailwindcss/vite": "^4.1.17", 59 | "svelte": "^5.43.8", 60 | "svelte-check": "^4.3.4", 61 | "tailwindcss": "^4.1.17", 62 | "typescript": "^5.9.3", 63 | "vite": "^7.2.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /dist/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from 'svelte'; 2 | /** 3 | * Represents a single line in the terminal history. 4 | */ 5 | export interface TerminalLine { 6 | type: 'command' | 'output' | 'error'; 7 | content: string | string[] | Component; 8 | id?: number; 9 | /** Optional snapshot of the prompt at the time this line was created (for commands). */ 10 | promptLabel?: string; 11 | } 12 | /** 13 | * Represents the virtual file system structure. 14 | * keys are file/directory names. 15 | * values are string (file content) or object (subdirectory). 16 | */ 17 | export type FileStructure = { 18 | [key: string]: string | FileStructure; 19 | }; 20 | /** 21 | * Function signature for custom commands. 22 | * Returns a promise that resolves to the output (string, lines, or void). 23 | */ 24 | export type CommandHandler = (args: string[]) => Promise | string | string[] | void; 25 | /** 26 | * Interface for the terminal theme. 27 | */ 28 | export interface Theme { 29 | background: string; 30 | foreground: string; 31 | prompt: string; 32 | cursor: string; 33 | selection?: string; 34 | } 35 | /** 36 | * Predefined themes available in the library. 37 | */ 38 | export declare const Themes: Record; 39 | /** 40 | * Props accepted by the `` component. 41 | */ 42 | export interface TerminalProps { 43 | /** Map of custom commands. Key is the command name. */ 44 | commands?: Record; 45 | /** Virtual file system structure. */ 46 | structure?: FileStructure; 47 | /** 48 | * Theme object or name of a preset ('dark', 'light', 'dracula', 'matrix'). 49 | * @default 'dark' 50 | */ 51 | theme?: keyof typeof Themes | Theme; 52 | /** Message to display on initialization. */ 53 | welcomeMessage?: string | string[]; 54 | /** Username for the prompt. @default 'user' */ 55 | user?: string; 56 | /** 57 | * Static override for the entire prompt string. 58 | * If provided, `user` and dynamic path are ignored. 59 | */ 60 | promptStr?: string; 61 | /** 62 | * Autoplay script for "Show Mode". 63 | * If provided, the terminal becomes read-only and plays this script. 64 | */ 65 | autoplay?: { 66 | command: string; 67 | output?: string | string[] | Component; 68 | }[]; 69 | /** 70 | * Whether to loop the autoplay script. 71 | * @default true 72 | */ 73 | autoplayLoop?: boolean; 74 | /** 75 | * Typing speed in milliseconds for autoplay. 76 | * @default 50 77 | */ 78 | typingSpeed?: number; 79 | /** 80 | * Additional CSS classes for the terminal container. 81 | */ 82 | class?: string; 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { Component } from 'svelte'; 3 | 4 | /** 5 | * Represents a single line in the terminal history. 6 | */ 7 | export interface TerminalLine { 8 | type: 'command' | 'output' | 'error'; 9 | content: string | string[] | Component; 10 | id?: number; 11 | /** Optional snapshot of the prompt at the time this line was created (for commands). */ 12 | promptLabel?: string; 13 | } 14 | 15 | /** 16 | * Represents the virtual file system structure. 17 | * keys are file/directory names. 18 | * values are string (file content) or object (subdirectory). 19 | */ 20 | export type FileStructure = { 21 | [key: string]: string | FileStructure; 22 | }; 23 | 24 | /** 25 | * Function signature for custom commands. 26 | * Returns a promise that resolves to the output (string, lines, or void). 27 | */ 28 | export type CommandHandler = (args: string[]) => Promise | string | string[] | void; 29 | 30 | /** 31 | * Interface for the terminal theme. 32 | */ 33 | export interface Theme { 34 | background: string; 35 | foreground: string; 36 | prompt: string; 37 | cursor: string; 38 | selection?: string; 39 | } 40 | 41 | /** 42 | * Predefined themes available in the library. 43 | */ 44 | export const Themes: Record = { 45 | dark: { 46 | background: '#1a1b26', 47 | foreground: '#a9b1d6', 48 | prompt: '#7aa2f7', 49 | cursor: '#c0caf5' 50 | }, 51 | light: { 52 | background: '#ffffff', 53 | foreground: '#000000', 54 | prompt: '#2563eb', 55 | cursor: '#000000' 56 | }, 57 | dracula: { 58 | background: '#282a36', 59 | foreground: '#f8f8f2', 60 | prompt: '#bd93f9', 61 | cursor: '#f8f8f2' 62 | }, 63 | matrix: { 64 | background: '#000000', 65 | foreground: '#00ff00', 66 | prompt: '#00cc00', 67 | cursor: '#00ff00' 68 | } 69 | }; 70 | 71 | /** 72 | * Props accepted by the `` component. 73 | */ 74 | export interface TerminalProps { 75 | /** Map of custom commands. Key is the command name. */ 76 | commands?: Record; 77 | 78 | /** Virtual file system structure. */ 79 | structure?: FileStructure; 80 | 81 | /** 82 | * Theme object or name of a preset ('dark', 'light', 'dracula', 'matrix'). 83 | * @default 'dark' 84 | */ 85 | theme?: keyof typeof Themes | Theme; 86 | 87 | /** Message to display on initialization. */ 88 | welcomeMessage?: string | string[]; 89 | 90 | /** Username for the prompt. @default 'user' */ 91 | user?: string; 92 | 93 | /** 94 | * Static override for the entire prompt string. 95 | * If provided, `user` and dynamic path are ignored. 96 | */ 97 | promptStr?: string; 98 | 99 | /** 100 | * Autoplay script for "Show Mode". 101 | * If provided, the terminal becomes read-only and plays this script. 102 | */ 103 | autoplay?: { command: string; output?: string | string[] | Component }[]; 104 | 105 | /** 106 | * Whether to loop the autoplay script. 107 | * @default true 108 | */ 109 | autoplayLoop?: boolean; 110 | 111 | /** 112 | * Typing speed in milliseconds for autoplay. 113 | * @default 50 114 | */ 115 | typingSpeed?: number; 116 | 117 | /** 118 | * Additional CSS classes for the terminal container. 119 | */ 120 | class?: string; 121 | } 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte Bash 2 | 3 |
4 | 5 | Svelte Bash Banner 6 | 7 |

Svelte Bash

8 | 9 |

10 | The ultimate lightweight, fully typed, and customizable terminal component for Svelte 5. 11 |

12 | 13 |

14 | Live Demo · 15 | NPM · 16 | GitHub 17 |

18 | 19 |

20 | NPM Version 21 | License 22 | Svelte 5 23 | Zero Dependency 24 |

25 | 26 |
27 | 28 | --- 29 | 30 | **Svelte Bash** is a high-performance terminal emulator component designed specifically for modern Svelte applications. It provides a realistic shell experience with a virtual file system, command history navigation, and advanced features like autoplay sequences for tutorials. 31 | 32 | > **Note:** As of v1.0.1, svelte-bash has been refactored to use **Pure Vanilla CSS** internally. This means it has **ZERO dependencies** on Tailwind CSS and will render correctly in any project (including Bootstrap, Tailwind, or plain CSS projects). You do NOT need to install Tailwind. 33 | 34 | Whether you are building a developer portfolio, a documentation site, or a web-based CLI tool, Svelte Bash offers the perfect balance of aesthetics and functionality. 35 | 36 | ## Key Features 37 | 38 | * **Lightweight & Fast**: Zero external dependencies, ~4kb gzipped. 39 | * **Virtual File System**: fully functional `ls`, `cd`, `cat`, and `pwd` commands. 40 | * **Deep Theming**: Includes `dracula`, `matrix`, and `dark` presets, plus full CSS control. 41 | * **Autoplay Mode**: Script commands to run automatically—perfect for landing page demos. 42 | * **Accessible**: Proper focus management and keyboard history navigation (Up/Down arrows). 43 | * **TypeScript**: Written in TypeScript for excellent type safety and autocomplete. 44 | 45 | ## Installation 46 | 47 | ```bash 48 | npm install svelte-bash 49 | ``` 50 | 51 | ## Usage 52 | 53 | ### Basic Example 54 | 55 | Import the component and pass a `structure` object to define the virtual file system. 56 | 57 | ```svelte 58 | 68 | 69 | 74 | ``` 75 | 76 | ### Custom Commands 77 | 78 | You can extend the terminal with your own commands by passing a `commands` object. 79 | 80 | ```svelte 81 | 98 | 99 | 100 | ``` 101 | 102 | ### Autoplay (Show Mode) 103 | 104 | Perfect for documentation or presentations. The terminal will automatically type and execute the provided sequence. 105 | 106 | ```svelte 107 | 113 | ``` 114 | 115 | ## Theming 116 | 117 | Svelte Bash allows comprehensive styling customization. 118 | 119 | **Built-in Presets:** 120 | - `dark` (default) 121 | - `light` 122 | - `dracula` 123 | - `matrix` 124 | 125 | **Custom Theme Object:** 126 | 127 | ```svelte 128 | 136 | ``` 137 | 138 | ## API Reference 139 | 140 | | Prop | Type | Default | Description | 141 | |------|------|---------|-------------| 142 | | `structure` | `FileStructure` | `{}` | Key-value pairs defining the virtual file system. | 143 | | `commands` | `Record` | `{}` | Custom command handlers. | 144 | | `theme` | `string` \| `Theme` | `'dark'` | Theme preset name or specific color object. | 145 | | `user` | `string` | `'user'` | The username displayed in the prompt. | 146 | | `machine` | `string` | `'machine'` | The machine name displayed in the prompt. | 147 | | `welcomeMessage` | `string` \| `string[]` | `[]` | Message shown on initialization. | 148 | | `autoplay` | `AutoplayItem[]` | `undefined` | Array of commands to execute automatically. | 149 | | `readonly` | `boolean` | `false` | If true, user input is disabled. | 150 | 151 | ## Contributing 152 | 153 | Contributions are welcome! Please feel free to submit a Pull Request. 154 | 155 | ## License 156 | 157 | MIT © [Yusuf Cengiz](https://github.com/YusufCeng1z) 158 | -------------------------------------------------------------------------------- /dist/shell.js: -------------------------------------------------------------------------------- 1 | export class Shell { 2 | structure; 3 | customCommands; 4 | cwd; 5 | history; 6 | user; 7 | constructor(structure = {}, commands = {}, user = 'user', history = []) { 8 | this.structure = structure; 9 | this.customCommands = commands; 10 | this.user = user; 11 | this.history = history; 12 | this.cwd = ['~']; 13 | } 14 | get currentPath() { 15 | return this.cwd; 16 | } 17 | setHistory(h) { 18 | this.history = h; 19 | } 20 | setStructure(s) { 21 | this.structure = s; 22 | } 23 | setCommands(c) { 24 | this.customCommands = c; 25 | } 26 | /** 27 | * Resolve path parts to a node in the file structure. 28 | */ 29 | resolveNode(pathParts) { 30 | let current = this.structure; 31 | // If path starts at root (not supported in this simple version unless we add '/' handling) 32 | // We assume structure IS the root. 33 | // We currently handle paths relative to root for simplification as `~` is implicit root here. 34 | // Actually, let's just traverse the structure object. 35 | for (const part of pathParts) { 36 | if (part === '~') 37 | continue; 38 | if (typeof current === 'object' && current[part]) { 39 | current = current[part]; 40 | } 41 | else { 42 | return undefined; 43 | } 44 | } 45 | return current; 46 | } 47 | /** 48 | * Process a command string and return the updated history lines (or void if direct history mutation). 49 | * Actually, let's return a list of NEW lines provided by the command execution. 50 | */ 51 | async execute(input) { 52 | const trimmed = input.trim(); 53 | if (!trimmed) 54 | return []; 55 | const [cmd, ...args] = trimmed.split(/\s+/); 56 | const outputLines = []; 57 | // 1. Built-in: clear 58 | if (cmd === 'clear') { 59 | // Signal to clear history. 60 | // We can return a special signal or just handle it in the component? 61 | // "Types" didn't specify a clear signal. 62 | // Let's assume the component checks for 'CLEAR_SIGNAL' or we act on history reference? 63 | // Cleaner: return a specific line type or handle it. 64 | // Let's just return a "system" type? Or rely on component to clear if it sees special property? 65 | // Actually, we can just return a 'clear' type line if we update types? 66 | // Or just return null? 67 | // "Professional": The shell should control history. 68 | // But Svelte reactivity means component owns the 'history' array usually. 69 | // Let's make this method just return the RESULT of the command. 70 | // Special handling for clear: 71 | return [{ type: 'command', content: 'CLEAR_SIGNAL', id: -1 }]; 72 | } 73 | // 2. Built-in: ls 74 | if (cmd === 'ls') { 75 | const ptr = this.resolveNode(this.cwd); 76 | if (ptr && typeof ptr === 'object') { 77 | const entries = Object.entries(ptr).map(([name, val]) => { 78 | return typeof val === 'object' ? name + '/' : name; 79 | }); 80 | outputLines.push({ type: 'output', content: entries.length ? entries.join(' ') : '' }); 81 | } 82 | else { 83 | outputLines.push({ type: 'error', content: `ls: cannot access '${this.cwd.join('/')}': Not a directory` }); 84 | } 85 | return outputLines; 86 | } 87 | // 3. Built-in: cat 88 | if (cmd === 'cat') { 89 | const target = args[0]; 90 | if (!target) { 91 | return [{ type: 'error', content: 'usage: cat ' }]; 92 | } 93 | // Resolve path relative to CWD 94 | // Simple 1-level check for now (or improve to full path parser) 95 | const ptr = this.resolveNode(this.cwd); 96 | if (ptr && typeof ptr === 'object' && ptr[target]) { 97 | const file = ptr[target]; 98 | if (typeof file === 'string') { 99 | return [{ type: 'output', content: file }]; 100 | } 101 | return [{ type: 'error', content: `cat: ${target}: Is a directory` }]; 102 | } 103 | return [{ type: 'error', content: `cat: ${target}: No such file` }]; 104 | } 105 | // 4. Built-in: cd 106 | if (cmd === 'cd') { 107 | const target = args[0]; 108 | if (!target || target === '~') { 109 | this.cwd = ['~']; 110 | return []; 111 | } 112 | if (target === '..') { 113 | if (this.cwd.length > 1) { 114 | this.cwd.pop(); 115 | } 116 | return []; 117 | } 118 | // Allow relative one-level down 119 | const ptr = this.resolveNode(this.cwd); 120 | if (ptr && typeof ptr === 'object' && ptr[target]) { 121 | if (typeof ptr[target] === 'object') { 122 | this.cwd.push(target); 123 | } 124 | else { 125 | return [{ type: 'error', content: `cd: ${target}: Not a directory` }]; 126 | } 127 | } 128 | else { 129 | return [{ type: 'error', content: `cd: ${target}: No such file or directory` }]; 130 | } 131 | return []; 132 | } 133 | // 5. Built-in: pwd 134 | if (cmd === 'pwd') { 135 | return [{ type: 'output', content: '/' + this.cwd.slice(1).join('/') }]; // slice(1) to ignore ~ as explicit root string 136 | } 137 | // 6. Built-in: help 138 | if (cmd === 'help') { 139 | const custom = Object.keys(this.customCommands); 140 | const content = `Available commands: clear, ls, cat, cd, pwd, help${custom.length ? ', ' + custom.join(', ') : ''}`; 141 | return [{ type: 'output', content }]; 142 | } 143 | // 7. Custom Commands 144 | if (this.customCommands[cmd]) { 145 | try { 146 | const res = await this.customCommands[cmd](args); 147 | if (res) 148 | return [{ type: 'output', content: res }]; 149 | return []; 150 | } 151 | catch (err) { 152 | return [{ type: 'error', content: err.message || String(err) }]; 153 | } 154 | } 155 | return [{ type: 'error', content: `Command not found: ${cmd}` }]; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/lib/shell.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { FileStructure, CommandHandler, TerminalLine } from './types'; 3 | 4 | export class Shell { 5 | private structure: FileStructure; 6 | private customCommands: Record; 7 | private cwd: string[]; 8 | private history: TerminalLine[]; 9 | private user: string; 10 | 11 | constructor( 12 | structure: FileStructure = {}, 13 | commands: Record = {}, 14 | user: string = 'user', 15 | history: TerminalLine[] = [] 16 | ) { 17 | this.structure = structure; 18 | this.customCommands = commands; 19 | this.user = user; 20 | this.history = history; 21 | this.cwd = ['~']; 22 | } 23 | 24 | public get currentPath(): string[] { 25 | return this.cwd; 26 | } 27 | 28 | public setHistory(h: TerminalLine[]) { 29 | this.history = h; 30 | } 31 | 32 | public setStructure(s: FileStructure) { 33 | this.structure = s; 34 | } 35 | 36 | public setCommands(c: Record) { 37 | this.customCommands = c; 38 | } 39 | 40 | /** 41 | * Resolve path parts to a node in the file structure. 42 | */ 43 | private resolveNode(pathParts: string[]): FileStructure | string | undefined { 44 | let current: FileStructure | string = this.structure; 45 | 46 | // If path starts at root (not supported in this simple version unless we add '/' handling) 47 | // We assume structure IS the root. 48 | // We currently handle paths relative to root for simplification as `~` is implicit root here. 49 | 50 | // Actually, let's just traverse the structure object. 51 | for (const part of pathParts) { 52 | if (part === '~') continue; 53 | if (typeof current === 'object' && current[part]) { 54 | current = current[part]; 55 | } else { 56 | return undefined; 57 | } 58 | } 59 | return current; 60 | } 61 | 62 | /** 63 | * Process a command string and return the updated history lines (or void if direct history mutation). 64 | * Actually, let's return a list of NEW lines provided by the command execution. 65 | */ 66 | public async execute(input: string): Promise { 67 | const trimmed = input.trim(); 68 | if (!trimmed) return []; 69 | 70 | const [cmd, ...args] = trimmed.split(/\s+/); 71 | const outputLines: TerminalLine[] = []; 72 | 73 | // 1. Built-in: clear 74 | if (cmd === 'clear') { 75 | // Signal to clear history. 76 | // We can return a special signal or just handle it in the component? 77 | // "Types" didn't specify a clear signal. 78 | // Let's assume the component checks for 'CLEAR_SIGNAL' or we act on history reference? 79 | // Cleaner: return a specific line type or handle it. 80 | // Let's just return a "system" type? Or rely on component to clear if it sees special property? 81 | // Actually, we can just return a 'clear' type line if we update types? 82 | // Or just return null? 83 | // "Professional": The shell should control history. 84 | // But Svelte reactivity means component owns the 'history' array usually. 85 | // Let's make this method just return the RESULT of the command. 86 | // Special handling for clear: 87 | return [{ type: 'command' as any, content: 'CLEAR_SIGNAL', id: -1 }]; 88 | } 89 | 90 | // 2. Built-in: ls 91 | if (cmd === 'ls') { 92 | const ptr = this.resolveNode(this.cwd); 93 | if (ptr && typeof ptr === 'object') { 94 | const entries = Object.entries(ptr).map(([name, val]) => { 95 | return typeof val === 'object' ? name + '/' : name; 96 | }); 97 | outputLines.push({ type: 'output', content: entries.length ? entries.join(' ') : '' }); 98 | } else { 99 | outputLines.push({ type: 'error', content: `ls: cannot access '${this.cwd.join('/')}': Not a directory` }); 100 | } 101 | return outputLines; 102 | } 103 | 104 | // 3. Built-in: cat 105 | if (cmd === 'cat') { 106 | const target = args[0]; 107 | if (!target) { 108 | return [{ type: 'error', content: 'usage: cat ' }]; 109 | } 110 | // Resolve path relative to CWD 111 | // Simple 1-level check for now (or improve to full path parser) 112 | const ptr = this.resolveNode(this.cwd); 113 | if (ptr && typeof ptr === 'object' && ptr[target]) { 114 | const file = ptr[target]; 115 | if (typeof file === 'string') { 116 | return [{ type: 'output', content: file }]; 117 | } 118 | return [{ type: 'error', content: `cat: ${target}: Is a directory` }]; 119 | } 120 | return [{ type: 'error', content: `cat: ${target}: No such file` }]; 121 | } 122 | 123 | // 4. Built-in: cd 124 | if (cmd === 'cd') { 125 | const target = args[0]; 126 | if (!target || target === '~') { 127 | this.cwd = ['~']; 128 | return []; 129 | } 130 | if (target === '..') { 131 | if (this.cwd.length > 1) { 132 | this.cwd.pop(); 133 | } 134 | return []; 135 | } 136 | 137 | // Allow relative one-level down 138 | const ptr = this.resolveNode(this.cwd); 139 | if (ptr && typeof ptr === 'object' && ptr[target]) { 140 | if (typeof ptr[target] === 'object') { 141 | this.cwd.push(target); 142 | } else { 143 | return [{ type: 'error', content: `cd: ${target}: Not a directory` }]; 144 | } 145 | } else { 146 | return [{ type: 'error', content: `cd: ${target}: No such file or directory` }]; 147 | } 148 | return []; 149 | } 150 | 151 | // 5. Built-in: pwd 152 | if (cmd === 'pwd') { 153 | return [{ type: 'output', content: '/' + this.cwd.slice(1).join('/') }]; // slice(1) to ignore ~ as explicit root string 154 | } 155 | 156 | // 6. Built-in: help 157 | if (cmd === 'help') { 158 | const custom = Object.keys(this.customCommands); 159 | const content = `Available commands: clear, ls, cat, cd, pwd, help${custom.length ? ', ' + custom.join(', ') : ''}`; 160 | return [{ type: 'output', content }]; 161 | } 162 | 163 | // 7. Custom Commands 164 | if (this.customCommands[cmd]) { 165 | try { 166 | const res = await this.customCommands[cmd](args); 167 | if (res) return [{ type: 'output', content: res as any }]; 168 | return []; 169 | } catch (err: any) { 170 | return [{ type: 'error', content: err.message || String(err) }]; 171 | } 172 | } 173 | 174 | return [{ type: 'error', content: `Command not found: ${cmd}` }]; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/lib/Terminal.svelte: -------------------------------------------------------------------------------- 1 | 172 | 173 | 177 |
inputElement?.focus()} 180 | role="button" 181 | tabindex="0" 182 | on:keydown={() => {}} 183 | {...$$restProps} 184 | class="svelte-bash-terminal custom-scrollbar {clazz}" 185 | style=" 186 | background-color: {activeTheme.background}; 187 | color: {activeTheme.foreground}; 188 | border-color: {activeTheme.prompt}33; 189 | {$$restProps.style || ''} 190 | " 191 | > 192 | 193 | {#each history as line (line.id)} 194 |
195 | {#if line.type === "command"} 196 | 197 | {line.promptLabel || "$"} 198 | 199 | {line.content} 200 | {:else if line.type === "error"} 201 | {line.content} 202 | {:else} 203 | 204 | {#if typeof line.content === "string"} 205 |
206 | {line.content} 207 |
208 | {:else if Array.isArray(line.content)} 209 | {#each line.content as item} 210 |
{item}
211 | {/each} 212 | {:else} 213 | 214 | {/if} 215 | {/if} 216 |
217 | {/each} 218 | 219 | 220 | {#if !autoplay} 221 |
222 | 226 | {promptStr || `${user}@host ${shell.currentPath.join("/")} $`} 227 | 228 | 238 |
239 | {:else} 240 |
241 | 245 | {promptStr || `${user}@host ${shell.currentPath.join("/")} $`} 246 | 247 | {currentInput} 248 | 252 |
253 | {/if} 254 |
255 | 256 | 365 | -------------------------------------------------------------------------------- /dist/Terminal.svelte: -------------------------------------------------------------------------------- 1 | 172 | 173 | 177 |
inputElement?.focus()} 180 | role="button" 181 | tabindex="0" 182 | on:keydown={() => {}} 183 | {...$$restProps} 184 | class="svelte-bash-terminal custom-scrollbar {clazz}" 185 | style=" 186 | background-color: {activeTheme.background}; 187 | color: {activeTheme.foreground}; 188 | border-color: {activeTheme.prompt}33; 189 | {$$restProps.style || ''} 190 | " 191 | > 192 | 193 | {#each history as line (line.id)} 194 |
195 | {#if line.type === "command"} 196 | 197 | {line.promptLabel || "$"} 198 | 199 | {line.content} 200 | {:else if line.type === "error"} 201 | {line.content} 202 | {:else} 203 | 204 | {#if typeof line.content === "string"} 205 |
206 | {line.content} 207 |
208 | {:else if Array.isArray(line.content)} 209 | {#each line.content as item} 210 |
{item}
211 | {/each} 212 | {:else} 213 | 214 | {/if} 215 | {/if} 216 |
217 | {/each} 218 | 219 | 220 | {#if !autoplay} 221 |
222 | 226 | {promptStr || `${user}@host ${shell.currentPath.join("/")} $`} 227 | 228 | 238 |
239 | {:else} 240 |
241 | 245 | {promptStr || `${user}@host ${shell.currentPath.join("/")} $`} 246 | 247 | {currentInput} 248 | 252 |
253 | {/if} 254 |
255 | 256 | 367 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 40 | 41 | 42 | Svelte Bash - The Terminal Component for Svelte 43 | 44 | 49 | 53 | 86 | 87 | 88 |
91 | 92 | 150 | 151 |
152 |
155 | 156 |
159 |
160 | 165 | 166 | 169 | 172 | 173 | v1.0.0 released 174 | 186 | 187 | 188 |

191 | The Terminal
192 | for Svelte. 196 |

197 | 198 |

201 | A beautifully typed, file-system ready terminal 202 | component. Give your app a CLI power-user interface in 203 | minutes. 204 |

205 | 206 |
207 | 213 | 218 | 228 | GitHub 229 | 230 |
231 |
232 |
233 | 234 | 235 |
236 | 237 |
240 | 241 | 242 |
245 | 246 |
249 |
250 |
253 |
256 |
259 |
260 |
263 | user@svelte-bash 264 | x 265 |
266 |
267 | 268 |
269 | 270 |
273 | 274 | 286 |
287 |
288 | 289 | 290 |
293 |
294 |
295 |
296 | 297 | 298 |
302 |
303 | 304 |
305 |
306 |
309 | 1 310 |
311 |

312 | Installation 313 |

314 |
315 |

316 | Get started by installing the package via npm. It has zero 317 | dependencies other than Svelte itself. 318 |

319 |
322 |
325 | npm install svelte-bash 326 | 332 |
333 |
334 |
335 | 336 | 337 |
338 |
339 |
342 | 2 343 |
344 |

345 | Basic Usage 346 |

347 |
348 |

349 | Import the component and pass a structure object 350 | to define your file system. 351 |

352 |
353 | 354 |
357 |
<script>
360 |   import {`{`} Terminal {`}`} from 'svelte-bash';
361 | 
362 |   const files = {`{`}
363 |     'readme.md': '# Hello World',
364 |     'src': {`{`}
365 |        'app.js': 'console.log("Hi")'
366 |     {`}`}
367 |   {`}`};
368 | </script>
369 | 
370 | <Terminal 
371 |     structure={`{`}files{`}`} 
372 |     user="alice" 
373 |     style="height: 300px"
374 | />
376 |
377 | 378 |
381 | 390 |
391 |
392 |
393 | 394 | 395 |
396 |
397 |
400 | 3 401 |
402 |

403 | Advanced Features 404 |

405 |
406 | 407 |
408 | 409 |
412 |
413 |

414 | Custom Commands 415 |

416 |

417 | Pass a commands object to handle custom 418 | logic. You can return a string, an array of lines, 419 | or even other Svelte components. 420 |

421 |
424 |
const cmds = {`{`}
429 |   greet: (args) => `Hello ${`{`}args[0]{`}`}`,
431 |   async: async () => {`{`}
432 |      await fetch('/api');
433 |      return 'Done!';
434 |   {`}`}
435 | {`}`};
436 | 
437 | <Terminal 
438 |    commands={`{`}cmds{`}`} 
439 |    style="height: 300px" 
440 | />
442 |
443 |
444 |
447 | 450 | `Hello ${args[0] || "Friend"}!`, 451 | time: () => new Date().toLocaleTimeString(), 452 | }} 453 | welcomeMessage="Type 'greet [name]' or 'time'" 454 | style="height: 250px; width: 100%;" 455 | /> 456 |
457 |
458 | 459 | 460 |
463 |
466 | 477 |
478 |
479 |

480 | Autoplay / Show Mode 481 |

482 |

483 | Great for tutorials. The terminal will type for 484 | you. 485 |

486 |
489 |
<Terminal 
492 |   autoplay={`{[`}
493 |     {`{`} command: 'git status' {`}`},
494 |     {`{`} 
495 |       command: 'git commit -m "wip"', 
496 |       output: '...' 
497 |     {`}`}
498 |   {`]}`}
499 |   style="height: 300px"
500 | />
502 |
503 |
504 |
505 | 506 | 507 |
510 |
511 |

512 | Deep Theming 513 |

514 |

515 | Override standard colors with the theme prop. 518 |

519 |
522 |
<Terminal 
525 |   theme={`{{`}
526 |     background: '#282a36',
527 |     prompt: '#ff79c6',
528 |     cursor: '#f8f8f2'
529 |   {`}}`}
530 |   style="height: 300px"
531 | />
533 |
534 |
535 |
538 | 548 |
549 |
550 |
551 |
552 | 553 | 554 |
555 |

API Reference

556 |
557 | 558 | 559 | 560 | 563 | 566 | 569 | 572 | 573 | 574 | 575 | 576 | 579 | 582 | 585 | 588 | 589 | 590 | 593 | 596 | 599 | 600 | 601 | 602 | 605 | 608 | 609 | 613 | 614 | 615 | 618 | 621 | 623 | 626 | 627 | 628 | 631 | 632 | 633 | 634 | 635 | 636 |
PropTypeDefaultDescription
structureFileStructure{`{}`}Virtual file system object key-value pairs.
commandsRecord<string, Handler>{`{}`}Custom command handlers.
themestring | Theme'dark'Preset name ('dark', 'light', 'dracula') or 611 | object.
autoplayAutoplayItem[]undefinedArray of commands to type automatically.
userstring'user'Username shown in prompt.
637 |
638 |
639 |
640 |
641 | 642 | 643 | 671 |
672 | --------------------------------------------------------------------------------