├── .nvmrc ├── .prettierignore ├── screenshot.png ├── .vscode ├── extensions.json └── settings.recommended.json ├── public ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── site.webmanifest └── vite.svg ├── src ├── vite-env.d.ts ├── utils │ ├── isNil.ts │ ├── colors │ │ ├── toFzfColorName.ts │ │ ├── hexColorToRgb.ts │ │ └── getContrastColor.ts │ ├── strings │ │ ├── htmlEscape.ts │ │ ├── urlHash.ts │ │ └── base64.ts │ ├── arrayChunk.ts │ ├── filterEmptyObjValues.ts │ ├── tui │ │ ├── mergeLines.ts │ │ ├── addSpacing.ts │ │ ├── Token.ts │ │ ├── createPreviewLines.ts │ │ ├── addBorders.ts │ │ ├── renderLines.ts │ │ ├── Line.ts │ │ └── createFinderLines.ts │ ├── addDelegateEventListener.ts │ ├── svelte │ │ ├── useDragScroll.ts │ │ └── usePopper.ts │ └── boxCoordinates.ts ├── styles │ ├── global-variables.scss │ ├── animations.css │ ├── forms.css │ ├── main.css │ ├── terminal.css │ └── reset.css ├── data │ ├── help │ │ ├── borderLabelPosition.md │ │ ├── margin.md │ │ └── padding.md │ ├── import │ │ ├── validateAndParseColors.ts │ │ ├── validateAndParseThemeOptions.ts │ │ ├── importFromUrlHash.ts │ │ ├── importFromEnvArgs.ts │ │ ├── validateAndParseThemeOptions.test.ts │ │ ├── importFromUrlHash.test.ts │ │ └── importFromEnvArgs.test.ts │ ├── options.schema.ts │ ├── options.store.ts │ ├── export │ │ ├── exportToUrlHash.ts │ │ ├── exportToUrlHash.test.ts │ │ └── exportToEnvVariable.ts │ ├── colors.schema.ts │ ├── fzfOptions.config.ts │ └── colors.store.ts ├── main.ts ├── setup.tests.ts ├── vendor.d.ts ├── components │ ├── common │ │ ├── FormControl.svelte │ │ ├── Box.svelte │ │ ├── Checkbox.svelte │ │ ├── InputCycle.svelte │ │ ├── InputWithHelp.svelte │ │ └── Modal.svelte │ ├── CustomColorPicker │ │ ├── ColorPickerWrapper.svelte │ │ └── TextInput.svelte │ ├── Palette.svelte │ ├── AboutPanel.svelte │ ├── ImportOptions.svelte │ ├── ColorPicker.svelte │ ├── PaletteColor.svelte │ ├── ExportOptions.svelte │ ├── Home.svelte │ ├── OptionsPanel.svelte │ └── TerminalWindow.svelte └── fzf │ ├── fzfBorders.ts │ └── fzfColorDefinitions.ts ├── .editorconfig ├── svelte.config.js ├── tsconfig.node.json ├── .yarnrc.yml ├── .gitignore ├── prettier.config.js ├── vite.config.ts ├── tsconfig.json ├── README.md ├── LICENSE ├── index.html ├── .eslintrc.json ├── .github └── workflows │ └── main.yml ├── package.json └── .yarn └── plugins └── @yarnpkg └── plugin-engines.cjs /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .yarn/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junegunn/fzf-themes/HEAD/screenshot.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junegunn/fzf-themes/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junegunn/fzf-themes/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junegunn/fzf-themes/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junegunn/fzf-themes/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junegunn/fzf-themes/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junegunn/fzf-themes/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/utils/isNil.ts: -------------------------------------------------------------------------------- 1 | export const isNil = (val: T | undefined | null): val is NonNullable => { 2 | return val === undefined || val === null; 3 | }; 4 | -------------------------------------------------------------------------------- /.vscode/settings.recommended.json: -------------------------------------------------------------------------------- 1 | { 2 | "[css]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "eslint.validate": ["typescript", "svelte"] 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/global-variables.scss: -------------------------------------------------------------------------------- 1 | // native css variables are not supported in media queries 2 | $mobile-breakpoint: 600px; 3 | $tablet-breakpoint: 900px; 4 | $desktop-breakpoint: 1200px; 5 | -------------------------------------------------------------------------------- /src/data/help/borderLabelPosition.md: -------------------------------------------------------------------------------- 1 | Position of the border label on the border line. Only available when 2 | both a `label` and `border` are defined. 3 | 4 | - `0` Center-aligned 5 | - `> 0` Align left 6 | - `< 0` Align right 7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Home from './components/Home.svelte'; 2 | 3 | import './styles/main.css'; 4 | 5 | const app = new Home({ 6 | target: document.getElementById('app') as HTMLElement, 7 | }); 8 | 9 | export default app; 10 | -------------------------------------------------------------------------------- /src/setup.tests.ts: -------------------------------------------------------------------------------- 1 | import failOnConsole from 'vitest-fail-on-console'; 2 | 3 | // force tests to fail if there are any console warnings/errors 4 | failOnConsole({ 5 | shouldFailOnError: true, 6 | shouldFailOnWarn: true, 7 | }); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /src/utils/colors/toFzfColorName.ts: -------------------------------------------------------------------------------- 1 | export const toFzfColorName = (name: string) => { 2 | return name.replace('-plus', '+'); 3 | }; 4 | 5 | export const toStoreColorName = (name: string) => { 6 | return name.replace('+', '-plus'); 7 | }; 8 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["vite.config.ts", "svelte.config.js", "prettier.config.js"] 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/strings/htmlEscape.ts: -------------------------------------------------------------------------------- 1 | export const escapeHtml = (str: string) => { 2 | return str 3 | .replace(/&/g, '&') 4 | .replace(//g, '>') 6 | .replace(/"/g, '"') 7 | .replace(/'/g, ''') 8 | .replace(/ /g, ' '); 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/arrayChunk.ts: -------------------------------------------------------------------------------- 1 | export function arrayChunk(arr: any[], chunkSize: number) { 2 | if (chunkSize <= 0) throw 'Invalid chunk size'; 3 | 4 | const chunks = []; 5 | 6 | for (let i = 0, len = arr.length; i < len; i += chunkSize) { 7 | chunks.push(arr.slice(i, i + chunkSize)); 8 | } 9 | 10 | return chunks; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/filterEmptyObjValues.ts: -------------------------------------------------------------------------------- 1 | export const filterEmptyObjValues = (obj: Record) => { 2 | const objNoEmptyValues: Record = {}; 3 | 4 | for (const prop in obj) { 5 | if (!obj[prop]) continue; 6 | 7 | objNoEmptyValues[prop] = obj[prop]; 8 | } 9 | 10 | return objNoEmptyValues; 11 | }; 12 | -------------------------------------------------------------------------------- /src/data/help/margin.md: -------------------------------------------------------------------------------- 1 | Comma-separated expression for margins around the finder. 2 | 3 | - `TRBL` Same margin for top, right, bottom, and left 4 | - `TB,RL` Vertical, horizontal margin 5 | - `T,RL,B` Top, horizontal, bottom margin 6 | - `T,R,B,L` Top, right, bottom, left margin 7 | 8 | `fzf` does allow percentages, but this tool **does not supported them**. 9 | -------------------------------------------------------------------------------- /src/utils/colors/hexColorToRgb.ts: -------------------------------------------------------------------------------- 1 | export const hexColorToRgb = (hexColor: string) => { 2 | const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexColor); 3 | 4 | return result 5 | ? { 6 | r: parseInt(result[1], 16), 7 | g: parseInt(result[2], 16), 8 | b: parseInt(result[3], 16), 9 | } 10 | : null; 11 | }; 12 | -------------------------------------------------------------------------------- /src/data/help/padding.md: -------------------------------------------------------------------------------- 1 | Comma-separated expression for padding inside the border. Padding is distinguishable from margin only when a border is used. 2 | 3 | - `TRBL` Same padding for top, right, bottom, and left 4 | - `TB,RL` Vertical, horizontal padding 5 | - `T,RL,B` Top, horizontal, bottom padding 6 | - `T,R,B,L` Top, right, bottom, left padding 7 | 8 | `fzf` does allow percentages, but this tool **does not supported them**. 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nmMode: classic 2 | 3 | nodeLinker: node-modules 4 | 5 | plugins: 6 | - checksum: 8f13acff9aef76f5fbfb5474b6d23b14ed3f49258cf4e344229e3515e42f4d6990e3c51b9ab176aa46c407fb9ca97fc0902c6400db5a44e9994d0b53512f3aed 7 | path: .yarn/plugins/@yarnpkg/plugin-engines.cjs 8 | spec: 'https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js' 9 | 10 | yarnPath: .yarn/releases/yarn-4.0.2.cjs 11 | -------------------------------------------------------------------------------- /src/vendor.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.md' { 2 | // "unknown" would be more detailed depends on how you structure frontmatter 3 | const attributes: Record; 4 | 5 | // When "Mode.HTML" is requested 6 | const html: string; 7 | 8 | // Modify below per your usage 9 | export { attributes, html }; 10 | } 11 | 12 | declare module 'yargs-parser/browser' { 13 | export * from 'yargs-parser'; 14 | export { default } from 'yargs-parser'; 15 | } 16 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/fzf-themes/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/fzf-themes/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/strings/urlHash.ts: -------------------------------------------------------------------------------- 1 | const ENC = { 2 | '+': '-', 3 | '/': '_', 4 | }; 5 | 6 | const DEC = { 7 | '-': '+', 8 | '_': '/', 9 | '.': '=', 10 | }; 11 | 12 | export const encodeHash = (base64: string) => { 13 | return base64.replace(/[+/]/g, (match: string) => 14 | match in ENC ? ENC[match as keyof typeof ENC] : match, 15 | ); 16 | }; 17 | 18 | export const decode = (safe: string) => { 19 | return safe.replace(/[-_.]/g, (match: string) => 20 | match in DEC ? DEC[match as keyof typeof DEC] : match, 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/colors/getContrastColor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Decide whether to use black/white text depending on bg color 3 | * 4 | * @source https://24ways.org/2010/calculating-color-contrast 5 | */ 6 | export const getContrastColor = (bgColor: string) => { 7 | const r = parseInt(bgColor.substring(1, 3), 16); 8 | const g = parseInt(bgColor.substring(3, 5), 16); 9 | const b = parseInt(bgColor.substring(5, 7), 16); 10 | 11 | const yiq = (r * 299 + g * 587 + b * 114) / 1000; 12 | 13 | return yiq >= 128 ? ('dark' as const) : ('light' as const); 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 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | !.vscode/settings.recommended.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | # Yarn with zero-installs 28 | .yarn/* 29 | !.yarn/cache 30 | !.yarn/patches 31 | !.yarn/plugins 32 | !.yarn/releases 33 | !.yarn/sdks 34 | !.yarn/versions 35 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | const config = { 3 | trailingComma: 'all', 4 | tabWidth: 2, 5 | semi: true, 6 | singleQuote: true, 7 | arrowParens: 'always', 8 | printWidth: 100, 9 | useTabs: false, 10 | quoteProps: 'consistent', 11 | bracketSameLine: false, 12 | // htmlWhitespaceSensitivity: 'ignore', 13 | svelteAllowShorthand: false, // explicit syntax is easier for beginners 14 | plugins: ['prettier-plugin-svelte'], 15 | overrides: [ 16 | { 17 | files: '*.svelte', 18 | options: { parser: 'svelte' }, 19 | }, 20 | ], 21 | }; 22 | 23 | export default config; 24 | -------------------------------------------------------------------------------- /src/utils/tui/mergeLines.ts: -------------------------------------------------------------------------------- 1 | import type { Line } from '~/utils/tui/Line'; 2 | 3 | export const mergeRenderedLines = (leftRenderedLines: Line[], rightRenderedLines: Line[]) => { 4 | const mergedLines: Line[] = []; 5 | 6 | // Find the number of lines to merge based on the longer array 7 | const numberOfLines = Math.max(leftRenderedLines.length, rightRenderedLines.length); 8 | 9 | for (let i = 0; i < numberOfLines; i++) { 10 | const mergedLine = leftRenderedLines[i].clone(false); 11 | mergedLine.tokens.push(...rightRenderedLines[i].tokens); 12 | 13 | mergedLines.push(mergedLine); 14 | } 15 | 16 | return mergedLines; 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/strings/base64.ts: -------------------------------------------------------------------------------- 1 | export function base64Encode(input: string): string { 2 | // Convert the string to UTF-8 using unescape and encodeURIComponent 3 | const utf8Input = unescape(encodeURIComponent(input)); 4 | let base64 = ''; 5 | for (let i = 0; i < utf8Input.length; i++) { 6 | base64 += String.fromCharCode(utf8Input.charCodeAt(i)); 7 | } 8 | return btoa(base64); 9 | } 10 | 11 | export function base64Decode(input: string): string { 12 | const base64 = atob(input); 13 | 14 | let utf8 = ''; 15 | for (let i = 0; i < base64.length; i++) { 16 | utf8 += '%' + ('00' + base64.charCodeAt(i).toString(16)).slice(-2); 17 | } 18 | 19 | return decodeURIComponent(utf8); 20 | } 21 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import mdPlugin, { Mode } from 'vite-plugin-markdown'; 3 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | base: '/fzf-themes', 8 | plugins: [svelte(), mdPlugin.plugin({ mode: [Mode.HTML] })], 9 | resolve: { 10 | alias: { 11 | '~': '/src', 12 | }, 13 | }, 14 | css: { 15 | preprocessorOptions: { 16 | scss: { 17 | // make SASS variables global by auto-importing them on top of each file 18 | additionalData: `@import "src/styles/global-variables.scss";`, 19 | }, 20 | }, 21 | }, 22 | test: { 23 | globals: true, 24 | setupFiles: ['src/setup.tests.ts'], 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /src/data/import/validateAndParseColors.ts: -------------------------------------------------------------------------------- 1 | import { colorsSchema } from '~/data/colors.schema'; 2 | import { initialColors, type ColorValues, isValidColor } from '~/data/colors.store'; 3 | 4 | export const validateAndParseColors = (rawObj: Record) => { 5 | if (!rawObj) return initialColors; 6 | 7 | const normalizedObj: Partial = {}; 8 | 9 | for (const key in colorsSchema.shape) { 10 | if (!isValidColor(key)) continue; 11 | 12 | const defaultValue = initialColors[key]; 13 | const receivedValue = rawObj[key] ?? defaultValue; 14 | 15 | const parsed = colorsSchema.shape[key as keyof ColorValues].safeParse(receivedValue); 16 | 17 | normalizedObj[key] = parsed.success ? receivedValue : defaultValue; 18 | } 19 | 20 | return normalizedObj as ColorValues; 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true, 17 | 18 | "baseUrl": ".", 19 | "paths": { 20 | "~/*": ["./src/*"] 21 | }, 22 | "types": ["vitest/globals"] 23 | }, 24 | "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /src/components/common/FormControl.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 43 | -------------------------------------------------------------------------------- /src/data/import/validateAndParseThemeOptions.ts: -------------------------------------------------------------------------------- 1 | import { themeOptionsSchema } from '~/data/options.schema'; 2 | import { 3 | initialOptions, 4 | isValidOption, 5 | type ThemeOption, 6 | type ThemeOptions, 7 | } from '~/data/options.store'; 8 | 9 | export const validateAndParseThemeOptions = (rawObj: Record) => { 10 | if (!rawObj) return initialOptions; 11 | 12 | const normalizedObj: Partial = {}; 13 | 14 | for (const key in themeOptionsSchema.shape) { 15 | if (!isValidOption(key)) continue; 16 | 17 | const defaultValue = initialOptions[key]; 18 | const receivedValue = rawObj[key] ?? defaultValue; 19 | 20 | const parsed = themeOptionsSchema.shape[key as ThemeOption].safeParse(receivedValue); 21 | 22 | normalizedObj[key] = parsed.success ? (parsed.data as any) : defaultValue; 23 | } 24 | 25 | return normalizedObj as ThemeOptions; 26 | }; 27 | -------------------------------------------------------------------------------- /src/styles/animations.css: -------------------------------------------------------------------------------- 1 | @keyframes cursor-blink { 2 | 0% { 3 | opacity: 0; 4 | } 5 | 6 | 50% { 7 | opacity: 1; 8 | } 9 | } 10 | 11 | @keyframes terminal-spinner { 12 | 10% { 13 | content: '⠙'; 14 | } 15 | 16 | 20% { 17 | content: '⠹'; 18 | } 19 | 20 | 30% { 21 | content: '⠸'; 22 | } 23 | 24 | 40% { 25 | content: '⠼'; 26 | } 27 | 28 | 50% { 29 | content: '⠴'; 30 | } 31 | 32 | 60% { 33 | content: '⠦'; 34 | } 35 | 36 | 70% { 37 | content: '⠧'; 38 | } 39 | 40 | 80% { 41 | content: '⠇'; 42 | } 43 | 44 | 90% { 45 | content: '⠏'; 46 | } 47 | } 48 | 49 | @keyframes modal-appear { 50 | from { 51 | transform: translateY(4%); 52 | opacity: 0; 53 | } 54 | 55 | to { 56 | transform: translateY(0); 57 | opacity: 1; 58 | } 59 | } 60 | 61 | @keyframes fade { 62 | from { 63 | opacity: 0; 64 | } 65 | 66 | to { 67 | opacity: 1; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/CustomColorPicker/ColorPickerWrapper.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
19 | 20 |
21 | 22 | 40 | -------------------------------------------------------------------------------- /src/utils/addDelegateEventListener.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery-style event delegation 3 | * https://youmightnotneedjquery.com/#on 4 | */ 5 | export const addDelegateEventListener = ( 6 | wrapperEl: HTMLElement, 7 | eventName: keyof HTMLElementEventMap, 8 | eventHandler: (event: Event) => void, 9 | selector: string, 10 | ) => { 11 | if (selector) { 12 | const wrappedHandler = (e: Event) => { 13 | if (!e.target) return; 14 | 15 | const triggeredEl = (e.target as HTMLElement).closest(selector); 16 | 17 | if (triggeredEl) { 18 | eventHandler.call(triggeredEl, e); 19 | } 20 | }; 21 | 22 | wrapperEl.addEventListener(eventName, wrappedHandler); 23 | 24 | return wrappedHandler; 25 | } else { 26 | const wrappedHandler = (e: Event) => { 27 | eventHandler.call(wrapperEl, e); 28 | }; 29 | 30 | wrapperEl.addEventListener(eventName, wrappedHandler); 31 | 32 | return wrappedHandler; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/data/options.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const themeOptionsSchema = z.object({ 4 | borderStyle: z 5 | .enum(['rounded', 'sharp', 'bold', 'double', 'block', 'thinblock', 'none']) 6 | .default('rounded'), 7 | borderLabel: z.string().optional(), 8 | borderLabelPosition: z.coerce.number().finite().default(0), 9 | previewBorderStyle: z 10 | .enum(['rounded', 'sharp', 'bold', 'double', 'block', 'thinblock']) 11 | .default('rounded'), 12 | margin: z.coerce 13 | .string() 14 | .regex(/^[0-9]+(,[0-9]+){0,3}$/) 15 | .default('0'), 16 | padding: z.coerce 17 | .string() 18 | .regex(/^[0-9]+(,[0-9]+){0,3}$/) 19 | .default('0'), 20 | prompt: z.string().default('> '), 21 | pointer: z.string().max(2).default('> '), 22 | marker: z.string().max(2).default('>'), 23 | separator: z.string().default('─'), 24 | scrollbar: z.string().max(1).default('│'), 25 | layout: z.enum(['default', 'reverse', 'reverse-list']).default('default'), 26 | info: z.enum(['default', 'right']).default('default'), 27 | }); 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fzf Theme Generator 2 | 3 | ![Front page of fzf Theme Playground](./screenshot.png) 4 | 5 | This tool allows you to quickly preview and tweak some of the many of the `fzf` ([repo](https://github.com/junegunn/fzf)) options/colors, 6 | to make it look just the way you like it. 7 | 8 | Made with Svelte. 9 | 10 | ## Export 11 | 12 | You can either export your options/colors to an `FZF_DEFAULT_OPTS` variable for your `.bashrc`, or 13 | share it with others through a url hash permalink. 14 | 15 | ## Known limitations 16 | 17 | This tool is meant to be a way to quickly preview `fzf` colors and settings, therefore 18 | not **all** options are supported. Some are quite hard to implement, or there is no point in recreating 19 | them here as the visual difference is minimal (e.g. margin/padding percentages). When you doubt, always consult `man fzf` for details! Some known unsupported features are: 20 | 21 | - margin/padding percentages 22 | - inline "info" options are missing 23 | - border top/bottom/left/right 24 | - and many more! 25 | 26 | Some of these might be added in the future when I have time, but PRs are also welcome. 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Vitor M 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/utils/tui/addSpacing.ts: -------------------------------------------------------------------------------- 1 | import type { BoxCoordinates } from '~/utils/boxCoordinates'; 2 | import { Line } from '~/utils/tui/Line'; 3 | import { token, fillSpace } from '~/utils/tui/Token'; 4 | 5 | const getVerticalSpace = (count: number, className?: string) => { 6 | if (count < 1) return []; 7 | 8 | const emptySpaceLine = new Line({ tokens: [fillSpace(' ', className)] }); 9 | 10 | return Array(count) 11 | .fill('') 12 | .map(() => emptySpaceLine.clone()); 13 | }; 14 | 15 | export const addSpacing = (lines: Line[], space: BoxCoordinates, className?: string) => { 16 | const linesWithSpacing = [ 17 | ...getVerticalSpace(space.top, className), 18 | ...lines.map((line) => { 19 | const lineClone = new Line({ tokens: line.tokens }); 20 | 21 | if (space.left > 0) { 22 | lineClone.tokens.unshift(token(' '.repeat(space.left), className)); 23 | } 24 | 25 | if (space.right > 0) { 26 | lineClone.tokens.push(token(' '.repeat(space.right), className)); 27 | } 28 | 29 | return lineClone; 30 | }), 31 | ...getVerticalSpace(space.bottom, className), 32 | ]; 33 | 34 | return linesWithSpacing; 35 | }; 36 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | fzf Theme Generator 8 | 9 | 10 | 11 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 |
34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["eslint:recommended", "plugin:svelte/recommended", "plugin:svelte/prettier"], 4 | "plugins": ["@typescript-eslint", "unicorn"], 5 | "parser": "@typescript-eslint/parser", 6 | "ignorePatterns": [ 7 | "node_modules/", 8 | "dist/", 9 | ".yarn/", 10 | "*.js", 11 | "prettier.config.js", 12 | "prettier.config.ts" 13 | ], 14 | "env": { 15 | "es2022": true, 16 | "browser": true 17 | }, 18 | "parserOptions": { 19 | "project": "tsconfig.json", 20 | "extraFileExtensions": [".svelte"] // This is a required setting in `@typescript-eslint/parser` v4.24.0. 21 | }, 22 | "rules": { 23 | "no-unused-vars": "off", 24 | "@typescript-eslint/no-unused-vars": "error" 25 | }, 26 | "overrides": [ 27 | { 28 | "files": ["*.svelte"], 29 | "parser": "svelte-eslint-parser", 30 | // Parse the ` 11 | 12 |
13 | {#if hasButtons || title} 14 |
15 | {#if title} 16 |

{title}

17 | {/if} 18 | 19 | {#if hasButtons} 20 |
21 | 22 |
23 | {/if} 24 |
25 | {/if} 26 | 27 |
28 |
29 | 30 | 65 | -------------------------------------------------------------------------------- /src/data/options.store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import { z } from 'zod'; 3 | import { themeOptionsSchema } from '~/data/options.schema'; 4 | import type { BorderStyle } from '~/fzf/fzfBorders'; 5 | 6 | export type ThemeOptions = z.infer; 7 | export type ThemeOption = keyof ThemeOptions; 8 | 9 | export const initialOptions: ThemeOptions = { 10 | borderStyle: 'rounded', 11 | borderLabel: '', 12 | borderLabelPosition: 0, 13 | previewBorderStyle: 'rounded', 14 | padding: '0', 15 | margin: '0', 16 | prompt: '> ', 17 | marker: '>', 18 | pointer: '◆', 19 | separator: '─', 20 | scrollbar: '│', 21 | layout: 'default', 22 | info: 'default', 23 | } as const; 24 | 25 | const _optionsStore = writable(initialOptions); 26 | 27 | export const optionsStore = { 28 | subscribe: _optionsStore.subscribe, 29 | set: (key: TKey, value: ThemeOptions[TKey]) => { 30 | _optionsStore.update((currentOptions) => ({ 31 | ...currentOptions, 32 | [key]: value, 33 | })); 34 | }, 35 | updateAll: (value: ThemeOptions) => { 36 | _optionsStore.update((currentOptions) => ({ 37 | ...currentOptions, 38 | ...value, 39 | })); 40 | }, 41 | setStyle: (style: BorderStyle) => { 42 | _optionsStore.update((currentOptions) => ({ 43 | ...currentOptions, 44 | borderStyle: style, 45 | })); 46 | }, 47 | }; 48 | 49 | export const isValidOption = (keyName: string): keyName is ThemeOption => { 50 | return keyName in initialOptions; 51 | }; 52 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ['main'] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: 'pages' 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Set up Node 34 | uses: actions/setup-node@v3 35 | with: 36 | node-version: '20.x' 37 | cache: 'yarn' 38 | 39 | - name: Install dependencies 40 | run: yarn install 41 | 42 | - run: yarn lint 43 | - run: yarn test 44 | 45 | - name: Build 46 | run: yarn build 47 | 48 | - name: Setup Pages 49 | uses: actions/configure-pages@v3 50 | 51 | - name: Upload artifacts 52 | uses: actions/upload-pages-artifact@v2 53 | with: 54 | path: './dist' 55 | 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v2 59 | -------------------------------------------------------------------------------- /src/data/export/exportToUrlHash.ts: -------------------------------------------------------------------------------- 1 | import { isValidColor, type ColorValues } from '~/data/colors.store'; 2 | import { type ThemeOption, type ThemeOptions } from '~/data/options.store'; 3 | import { toFzfColorName } from '~/utils/colors/toFzfColorName'; 4 | import { base64Encode } from '~/utils/strings/base64'; 5 | 6 | export const encodeObjForUrlHash = (obj: Record) => { 7 | return base64Encode(JSON.stringify(obj)); 8 | }; 9 | 10 | export const exportToUrlHash = ( 11 | themeOptions: ThemeOptions, 12 | colors: ColorValues, 13 | colorsOnly: boolean, 14 | ) => { 15 | const colorVariables: Map = new Map(); 16 | 17 | Object.entries(colors).forEach(([name, value]) => { 18 | if (!isValidColor(name)) return; 19 | 20 | const fzfColorName = toFzfColorName(name); 21 | 22 | if (value) { 23 | colorVariables.set(fzfColorName, value); 24 | } 25 | }); 26 | 27 | const colorsString = [...colorVariables.keys()] 28 | .map((color) => { 29 | return `${color}:${colorVariables.get(color)}`; 30 | }) 31 | .join(','); 32 | 33 | const optionsForEnv = Object.fromEntries( 34 | Object.keys(themeOptions).map((option) => { 35 | return [option, themeOptions[option as ThemeOption]]; 36 | }), 37 | ); 38 | 39 | const exportedObj = { 40 | ...(!colorsOnly ? optionsForEnv : undefined), 41 | colors: colorsString, 42 | }; 43 | 44 | const host = `${document.location.protocol}//${document.location.host}`; 45 | const path = import.meta.env.BASE_URL; 46 | 47 | return `${host}${path}#${encodeObjForUrlHash(exportedObj)}`; 48 | }; 49 | -------------------------------------------------------------------------------- /src/utils/tui/Token.ts: -------------------------------------------------------------------------------- 1 | import { escapeHtml } from '~/utils/strings/htmlEscape'; 2 | 3 | export class Token { 4 | public readonly text: string; 5 | private classNames: string | undefined; 6 | 7 | constructor(text: string, classNames?: string) { 8 | this.text = text; 9 | this.classNames = classNames; 10 | } 11 | 12 | addClass(className: string | undefined) { 13 | if (className && !this.classNames?.split(' ').includes(className)) { 14 | this.classNames = [this.classNames, className].filter(Boolean).join(' '); 15 | } 16 | 17 | return this; 18 | } 19 | 20 | length() { 21 | return this.text.length; 22 | } 23 | 24 | render(extraClassName = '') { 25 | const el = document.createElement('span'); 26 | el.innerHTML = escapeHtml(this.text); 27 | 28 | if (this.classNames) { 29 | el.className = [this.classNames, extraClassName].filter(Boolean).join(' '); 30 | } 31 | 32 | return el; 33 | } 34 | } 35 | 36 | /** 37 | * Convenience method for "new Token()" 38 | */ 39 | export const token = (text: string, classNames?: string) => { 40 | return new Token(text, classNames); 41 | }; 42 | 43 | export class FillSpace { 44 | public readonly fillChar: string; 45 | public readonly classNames: string | undefined; 46 | 47 | constructor(fillChar: string, classNames?: string) { 48 | this.fillChar = fillChar; 49 | this.classNames = classNames; 50 | } 51 | } 52 | 53 | /** 54 | * Convenience method for "new FillSpace()" 55 | */ 56 | export const fillSpace = (fillChar: string, classNames?: string) => { 57 | return new FillSpace(fillChar, classNames); 58 | }; 59 | -------------------------------------------------------------------------------- /src/data/colors.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | const hexColor = z.string().startsWith('#').length(7); 4 | const hexColorOptional = z 5 | .union([z.string().startsWith('#').length(7), z.literal('')]) 6 | .default('') 7 | .catch(''); 8 | 9 | export const colorsSchema = z.object({ 10 | 'fg': hexColor.default('#d0d0d0').catch('#d0d0d0'), 11 | 'fg-plus': hexColor.default('#d0d0d0').catch('#d0d0d0'), 12 | 'bg': hexColor.default('#121212').catch('#121212'), 13 | 'bg-plus': hexColor.default('#262626').catch('#262626'), 14 | 'hl': hexColor.default('#5f87af').catch('#5f87af'), 15 | 'hl-plus': hexColor.default('#5fd7ff').catch('#5fd7ff'), 16 | 'info': hexColor.default('#afaf87').catch('#afaf87'), 17 | 'marker': hexColor.default('#87ff00').catch('#87ff00'), 18 | 'prompt': hexColor.default('#d7005f').catch('#d7005f'), 19 | 'spinner': hexColor.default('#af5fff').catch('#af5fff'), 20 | 'pointer': hexColor.default('#af5fff').catch('#af5fff'), 21 | 'header': hexColor.default('#87afaf').catch('#87afaf'), 22 | 'gutter': hexColorOptional, 23 | 'border': hexColor.default('#262626').catch('#262626'), 24 | 'separator': hexColorOptional, 25 | 'scrollbar': hexColorOptional, 26 | 'preview-fg': hexColorOptional, 27 | 'preview-bg': hexColorOptional, 28 | 'preview-border': hexColorOptional, 29 | 'preview-scrollbar': hexColorOptional, 30 | 'preview-label': hexColorOptional, 31 | 'label': hexColor.default('#aeaeae').catch('#aeaeae'), 32 | 'query': hexColor.default('#d9d9d9').catch('#d9d9d9'), 33 | 'disabled': hexColorOptional, 34 | }); 35 | 36 | export type ColorName = keyof z.infer; 37 | -------------------------------------------------------------------------------- /src/data/import/importFromUrlHash.ts: -------------------------------------------------------------------------------- 1 | import { colorsSchema, type ColorName } from '~/data/colors.schema'; 2 | import { initialColors, type ColorValues } from '~/data/colors.store'; 3 | import { validateAndParseColors } from '~/data/import/validateAndParseColors'; 4 | import { validateAndParseThemeOptions } from '~/data/import/validateAndParseThemeOptions'; 5 | import { initialOptions, type ThemeOptions } from '~/data/options.store'; 6 | import { toStoreColorName } from '~/utils/colors/toFzfColorName'; 7 | import { base64Decode } from '~/utils/strings/base64'; 8 | 9 | type ImportFromUrlHashOutput = { 10 | colors: ColorValues; 11 | themeOptions: ThemeOptions; 12 | }; 13 | 14 | export const importFromUrlHash = (hash: string): ImportFromUrlHashOutput => { 15 | let themeOptions; 16 | 17 | try { 18 | const jsonString = base64Decode(hash); 19 | themeOptions = JSON.parse(jsonString); 20 | } catch { 21 | return { themeOptions: initialOptions, colors: initialColors }; 22 | } 23 | 24 | const colorsStr = themeOptions.colors as string; 25 | 26 | let colors = initialColors; 27 | 28 | if (colorsStr && String(colorsStr).includes(',')) { 29 | const rawColorObj = Object.fromEntries( 30 | colorsStr.split(',').map((colorPair) => { 31 | const [name, value] = colorPair.split(':'); 32 | 33 | return [toStoreColorName(name) as ColorName, value as string]; 34 | }), 35 | ); 36 | 37 | colors = colorsSchema.parse(rawColorObj); 38 | } 39 | 40 | delete themeOptions.colors; 41 | 42 | return { 43 | themeOptions: validateAndParseThemeOptions(themeOptions), 44 | colors: validateAndParseColors(colors), 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/Palette.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 11 | 12 |
13 | {#each orderedColorTokens as token} 14 | 15 | {/each} 16 |
17 |
18 | 19 | 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fzf-svelte", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "test": "vitest --environment=jsdom", 11 | "check": "svelte-check --tsconfig ./tsconfig.json", 12 | "lint": "eslint --cache --cache-location node_modules/.eslintcache . && yarn check", 13 | "format": "prettier --write ." 14 | }, 15 | "devDependencies": { 16 | "@sveltejs/vite-plugin-svelte": "^3.0.1", 17 | "@tsconfig/svelte": "^5.0.2", 18 | "@types/yargs-parser": "^21.0.3", 19 | "@typescript-eslint/eslint-plugin": "^6.17.0", 20 | "@typescript-eslint/parser": "^6.17.0", 21 | "@zerodevx/svelte-toast": "^0.9.5", 22 | "eslint": "^8.56.0", 23 | "eslint-config-prettier": "^9.1.0", 24 | "eslint-plugin-svelte": "^2.35.1", 25 | "eslint-plugin-unicorn": "^50.0.1", 26 | "jsdom": "^23.2.0", 27 | "prettier": "^3.1.1", 28 | "prettier-plugin-svelte": "^3.1.2", 29 | "sass": "^1.69.7", 30 | "svelte": "^4.2.8", 31 | "svelte-check": "^3.6.2", 32 | "svelte-ionicons": "^0.7.2", 33 | "tslib": "^2.6.2", 34 | "typescript": "^5.3.3", 35 | "vite": "^5.0.8", 36 | "vite-plugin-markdown": "^2.1.0", 37 | "vitest": "^1.2.1", 38 | "vitest-fail-on-console": "^0.5.1" 39 | }, 40 | "dependencies": { 41 | "@popperjs/core": "^2.11.8", 42 | "svelte-awesome-color-picker": "^3.0.0-beta.11", 43 | "svelte-select": "^5.8.3", 44 | "yargs-parser": "^21.1.1", 45 | "zod": "^3.22.4" 46 | }, 47 | "engines": { 48 | "node": "^20.0.0" 49 | }, 50 | "packageManager": "yarn@4.0.2" 51 | } 52 | -------------------------------------------------------------------------------- /src/components/common/Checkbox.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 74 | -------------------------------------------------------------------------------- /src/utils/boxCoordinates.ts: -------------------------------------------------------------------------------- 1 | const emptyCoords = { top: 0, right: 0, bottom: 0, left: 0 }; 2 | 3 | export type BoxCoordinates = { 4 | top: number; 5 | bottom: number; 6 | left: number; 7 | right: number; 8 | }; 9 | 10 | export const boxCoordinatesToString = (coords: BoxCoordinates) => { 11 | const isSameVertical = coords.top === coords.bottom; 12 | const isSameHorizontal = coords.left === coords.right; 13 | const isAllSame = isSameHorizontal && isSameVertical && coords.top === coords.left; 14 | 15 | if (isAllSame) { 16 | return String(coords.top); 17 | } else if (isSameVertical && isSameHorizontal) { 18 | return `${coords.top},${coords.left}`; 19 | } else if (isSameHorizontal) { 20 | return `${coords.top},${coords.left},${coords.bottom}`; 21 | } else { 22 | return `${coords.top},${coords.right},${coords.bottom},,${coords.left}`; 23 | } 24 | }; 25 | 26 | export const stringToBoxCoordinates = (coordsStr: string) => { 27 | const coords = String(coordsStr) 28 | .split(',') 29 | .map((i) => Math.min(Number.parseInt(i, 10), 50)); 30 | 31 | if (coords.some((i) => Number.isNaN(i))) return emptyCoords; 32 | 33 | if (!coordsStr) { 34 | return emptyCoords; 35 | } 36 | 37 | switch (coords.length) { 38 | case 4: 39 | return { top: coords[0], right: coords[1], bottom: coords[2], left: coords[3] }; 40 | case 3: 41 | return { top: coords[0], right: coords[1], bottom: coords[2], left: coords[1] }; 42 | case 2: 43 | return { top: coords[0], right: coords[1], bottom: coords[0], left: coords[1] }; 44 | case 1: { 45 | return { top: coords[0], right: coords[0], bottom: coords[0], left: coords[0] }; 46 | } 47 | default: 48 | return emptyCoords; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/data/fzfOptions.config.ts: -------------------------------------------------------------------------------- 1 | import { type ThemeOption, type ThemeOptions } from '~/data/options.store'; 2 | 3 | export type FzfOptionDefinition = { 4 | argName: string; 5 | exportIf?: (val: string, allOptions: ThemeOptions) => boolean; 6 | transformExport?: (val: ThemeOptions[T], allOptions: ThemeOptions) => string; 7 | transformImport?: (val: any) => string; 8 | }; 9 | 10 | export type FzfOptions = { 11 | [K in ThemeOption]: FzfOptionDefinition; 12 | }; 13 | 14 | export const fzfOptionsConfig: FzfOptions = { 15 | margin: { 16 | argName: 'margin', 17 | exportIf: (val) => val !== '0', 18 | }, 19 | padding: { 20 | argName: 'padding', 21 | exportIf: (val) => val !== '0', 22 | }, 23 | borderStyle: { 24 | argName: 'border', 25 | exportIf: (val) => val !== 'none', 26 | }, 27 | borderLabel: { 28 | argName: 'border-label', 29 | exportIf: (_, allOptions) => allOptions.borderStyle !== 'none', 30 | }, 31 | borderLabelPosition: { 32 | argName: 'border-label-pos', 33 | exportIf: (_, allOptions) => !!allOptions.borderLabel, 34 | }, 35 | previewBorderStyle: { 36 | argName: 'preview-window', 37 | transformExport: (val) => `border-${val}`, 38 | transformImport: (val: unknown) => String(val).substring(7), // remove "border-" prefix 39 | }, 40 | separator: { 41 | argName: 'separator', 42 | }, 43 | scrollbar: { 44 | argName: 'scrollbar', 45 | }, 46 | prompt: { 47 | argName: 'prompt', 48 | }, 49 | pointer: { 50 | argName: 'pointer', 51 | }, 52 | marker: { 53 | argName: 'marker', 54 | }, 55 | layout: { 56 | argName: 'layout', 57 | exportIf: (val) => val !== 'default', 58 | }, 59 | info: { 60 | argName: 'info', 61 | exportIf: (val) => val !== 'default', 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /src/utils/tui/createPreviewLines.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOptions } from '~/data/options.store'; 2 | import { Line } from '~/utils/tui/Line'; 3 | import { token, fillSpace } from '~/utils/tui/Token'; 4 | import { addBorders } from '~/utils/tui/addBorders'; 5 | import { addSpacing } from '~/utils/tui/addSpacing'; 6 | 7 | export const createPreviewLines = (themeOptions: ThemeOptions) => { 8 | let previewLines = [ 9 | new Line({ 10 | className: 'preview-bg', 11 | tokens: [ 12 | token('package fzf', 'preview-fg'), 13 | fillSpace(' ', 'preview-bg'), 14 | token(themeOptions.scrollbar, 'preview-scrollbar'), 15 | ], 16 | }), 17 | new Line({ 18 | className: 'preview-bg', 19 | tokens: [fillSpace(' ', 'preview-bg'), token(themeOptions.scrollbar, 'preview-scrollbar')], 20 | }), 21 | new Line({ className: 'preview-bg', tokens: [token('import (', 'preview-fg')] }), 22 | new Line({ className: 'preview-bg', tokens: [token(' "errors"', 'preview-fg')] }), 23 | new Line({ className: 'preview-bg', tokens: [token(' "os"', 'preview-fg')] }), 24 | new Line({ className: 'preview-bg', tokens: [token(' "strings"', 'preview-fg')] }), 25 | new Line({ className: 'preview-bg', tokens: [token(')', 'preview-fg')] }), 26 | new Line({ 27 | className: 'preview-bg', 28 | tokens: [token('// History struct ', 'preview-fg')], 29 | }), 30 | ]; 31 | 32 | for (const line of previewLines) { 33 | line.ensureContainsFillSpace(); 34 | } 35 | 36 | previewLines = addSpacing(previewLines, { top: 0, bottom: 0, left: 1, right: 0 }, 'preview-bg'); 37 | previewLines = addBorders(previewLines, { 38 | style: themeOptions.previewBorderStyle, 39 | label: '', 40 | position: 0, 41 | className: 'preview-bg preview-border', 42 | }); 43 | 44 | return previewLines; 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/AboutPanel.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 | 9 | 12 |
Theme Generator
13 |
14 |
15 | 16 | 17 |
18 | Heavily inspired by terminal.sexy, this offers a quick way to preview *some* of the many styling options 21 | available for fzf. Create & share your themes. 22 |

Made with 23 | 24 | by 25 | @vitormv. 26 |
27 | Checkout the source code at Github 28 |
29 |
30 |
31 | 32 | 64 | -------------------------------------------------------------------------------- /src/styles/forms.css: -------------------------------------------------------------------------------- 1 | input:not([type]), 2 | input { 3 | color: var(--text-color); 4 | background: var(--bg-color); 5 | border: 2px solid var(--border-color); 6 | padding: 3px 5px; 7 | line-height: 24px; 8 | outline: 0; 9 | box-sizing: border-box; 10 | 11 | &:focus { 12 | border-color: var(--border-dark-color); 13 | } 14 | 15 | &:invalid { 16 | border: 2px solid #cc383f; 17 | } 18 | 19 | &::placeholder { 20 | font-style: italic; 21 | font-family: monospace; 22 | } 23 | } 24 | 25 | textarea { 26 | padding: 15px; 27 | color: var(--text-color); 28 | background: var(--bg-color); 29 | border: 2px solid var(--border-color); 30 | 31 | &:focus { 32 | outline: 2px solid var(--border-dark-color); 33 | } 34 | } 35 | 36 | .btn { 37 | display: inline-flex; 38 | align-items: center; 39 | padding: 4px 10px; 40 | border-radius: 4px; 41 | cursor: pointer; 42 | line-height: 1.5; 43 | transition: 44 | color 150ms ease-out, 45 | background-color 150ms ease-out, 46 | border-color 150ms ease-out; 47 | 48 | &.btn-primary { 49 | background-color: var(--color-primary); 50 | color: var(--text-color); 51 | border: 0; 52 | 53 | &:hover:not(:disabled), 54 | &:focus:not(:disabled) { 55 | background-color: var(--color-primary-stronger); 56 | } 57 | } 58 | 59 | &.btn-secondary { 60 | background-color: var(--color-secondary); 61 | color: var(--text-color); 62 | border: 0; 63 | 64 | &:hover:not(:disabled), 65 | &:focus:not(:disabled) { 66 | background-color: var(--color-secondary-stronger); 67 | } 68 | } 69 | 70 | &.btn-outline { 71 | color: var(--text-color); 72 | background-color: transparent; 73 | border: 2px solid var(--border-color); 74 | 75 | &:hover:not(:disabled), 76 | &:focus:not(:disabled) { 77 | border-color: var(--border-darker-color); 78 | } 79 | } 80 | 81 | &:active:not(:disabled) { 82 | transform: translateY(1px); 83 | } 84 | 85 | &:disabled { 86 | opacity: 0.5; 87 | cursor: not-allowed; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | @import url('./reset'); 2 | @import url('./animations.css'); 3 | @import url('./forms'); 4 | @import url('./terminal'); 5 | 6 | :root { 7 | --brand-text: #e9e9e9; 8 | --brand-bg: #3f3f3f; 9 | --brand-marker: #f80069; 10 | --brand-highlight: #b0e1bd; 11 | --brand-font-family: 'Contrail One', sans-serif; 12 | 13 | --color-primary: #de3c4b; 14 | --color-primary-stronger: #c02130; 15 | --color-secondary: #0892a5; 16 | --color-secondary-stronger: #067584; 17 | --bg-color: #3f3f3f; 18 | --text-color: #e9e9e9; 19 | --box-gap: 15px; 20 | --box-bg-color: #323232; 21 | 22 | --border-color: #3f3f3f; 23 | --border-dark-color: #787878; 24 | 25 | --gray-900: #202020; 26 | 27 | line-height: 1.5; 28 | font-weight: 400; 29 | 30 | color-scheme: light dark; 31 | color: rgba(255, 255, 255, 0.87); 32 | 33 | font-synthesis: none; 34 | text-rendering: optimizeLegibility; 35 | -webkit-font-smoothing: antialiased; 36 | -moz-osx-font-smoothing: grayscale; 37 | } 38 | 39 | * { 40 | box-sizing: border-box; 41 | } 42 | 43 | body { 44 | margin: 0; 45 | touch-action: none; 46 | min-width: 320px; 47 | padding: var(--box-gap); 48 | height: 100vh; 49 | min-height: 830px; 50 | background-color: var(--bg-color); 51 | color: var(--text-color); 52 | font-family: 'Inconsolata', monospace; 53 | } 54 | 55 | @media (min-height: 860px) and (max-height: 1080px) { 56 | body { 57 | min-height: auto; 58 | height: 100vh; 59 | } 60 | } 61 | 62 | @media (min-height: 1080px) { 63 | body { 64 | min-height: auto; 65 | height: 1080px; 66 | } 67 | } 68 | 69 | #app { 70 | height: 100%; 71 | } 72 | 73 | a { 74 | font-weight: 500; 75 | color: #4ca0f4; 76 | text-decoration: underline; 77 | } 78 | 79 | a:hover { 80 | color: #535bf2; 81 | text-decoration: none; 82 | } 83 | 84 | .hidden { 85 | display: none; 86 | } 87 | 88 | .is-being-dragged { 89 | user-select: none !important; 90 | 91 | > .outer-spacing { 92 | pointer-events: none; 93 | } 94 | } 95 | 96 | :root { 97 | --toastContainerTop: auto; 98 | --toastContainerRight: auto; 99 | --toastContainerBottom: 2rem; 100 | --toastContainerLeft: calc(50vw - 8rem); 101 | } 102 | -------------------------------------------------------------------------------- /src/data/import/importFromEnvArgs.ts: -------------------------------------------------------------------------------- 1 | import parser from 'yargs-parser/browser'; 2 | import type { Options } from 'yargs-parser'; 3 | import { colorsStore } from '~/data/colors.store'; 4 | import { fzfOptionsConfig } from '~/data/fzfOptions.config'; 5 | import { validateAndParseColors } from '~/data/import/validateAndParseColors'; 6 | import { validateAndParseThemeOptions } from '~/data/import/validateAndParseThemeOptions'; 7 | import { 8 | isValidOption, 9 | optionsStore, 10 | type ThemeOption, 11 | type ThemeOptions, 12 | } from '~/data/options.store'; 13 | import { toFzfColorName } from '~/utils/colors/toFzfColorName'; 14 | 15 | const yargsParserOptions: Options = { 16 | configuration: { 17 | 'parse-numbers': false, 18 | 'camel-case-expansion': false, 19 | }, 20 | }; 21 | 22 | export const parseEnvArgs = (argsStr: string) => { 23 | const parsedObj = parser(argsStr.replace(/\n/g, ' '), yargsParserOptions); 24 | const themeOptions: Partial = {}; 25 | const colors: Record = {}; 26 | 27 | for (const argKey in parsedObj) { 28 | if (argKey === 'color') { 29 | if (Array.isArray(parsedObj[argKey])) { 30 | const splitColors = String(parsedObj[argKey]).split(','); 31 | 32 | for (const color of splitColors) { 33 | let [colorName, colorValue] = String(color).split(':'); 34 | 35 | colorValue = String(colorValue).trim(); 36 | 37 | colors[toFzfColorName(colorName)] = colorValue === '-1' ? '' : colorValue; 38 | } 39 | } 40 | } 41 | 42 | const optionName = Object.keys(fzfOptionsConfig).find( 43 | (i) => fzfOptionsConfig[i as ThemeOption]?.argName === argKey, 44 | ); 45 | 46 | if (!optionName || !isValidOption(optionName)) continue; 47 | 48 | themeOptions[optionName] = 49 | fzfOptionsConfig[optionName].transformImport?.(parsedObj[argKey]) ?? parsedObj[argKey]; 50 | } 51 | 52 | return { 53 | themeOptions: validateAndParseThemeOptions(themeOptions), 54 | colors: validateAndParseColors(colors), 55 | }; 56 | }; 57 | 58 | export const importFromEnvArgs = (argsStr: string) => { 59 | const parsed = parseEnvArgs(argsStr); 60 | 61 | optionsStore.updateAll(parsed.themeOptions); 62 | colorsStore.updateAllColors(parsed.colors); 63 | 64 | return parsed; 65 | }; 66 | -------------------------------------------------------------------------------- /src/data/import/validateAndParseThemeOptions.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, describe } from 'vitest'; 2 | import { validateAndParseThemeOptions } from '~/data/import/validateAndParseThemeOptions'; 3 | 4 | import { initialOptions, type ThemeOption, type ThemeOptions } from '~/data/options.store'; 5 | 6 | const sampleThemeOptions: ThemeOptions = { 7 | borderStyle: 'block', 8 | borderLabel: ' Geralt of Rivia ', 9 | borderLabelPosition: 10, 10 | previewBorderStyle: 'double', 11 | padding: '2', 12 | margin: '3', 13 | prompt: 'Here: ', 14 | marker: '->', 15 | pointer: '*', 16 | separator: 'LALA', 17 | scrollbar: '+', 18 | layout: 'reverse', 19 | info: 'right', 20 | }; 21 | 22 | describe('validateAndParseThemeOptions()', () => { 23 | it('should parse complete object', () => { 24 | const output = validateAndParseThemeOptions(sampleThemeOptions); 25 | 26 | expect(output).toEqual(sampleThemeOptions); 27 | }); 28 | 29 | it('can handle weird types', () => { 30 | expect(validateAndParseThemeOptions('WAT' as any)).toEqual(initialOptions); 31 | expect(validateAndParseThemeOptions(undefined as any)).toEqual(initialOptions); 32 | expect(validateAndParseThemeOptions(null as any)).toEqual(initialOptions); 33 | expect(validateAndParseThemeOptions(new Date() as any)).toEqual(initialOptions); 34 | }); 35 | 36 | it('can handle partial objects', () => { 37 | const partial = { 38 | borderLabel: 'Geralt of Rivia', 39 | }; 40 | 41 | const output = validateAndParseThemeOptions(partial); 42 | 43 | expect(output).toEqual({ 44 | ...initialOptions, 45 | borderLabel: 'Geralt of Rivia', 46 | }); 47 | }); 48 | 49 | it('should recover from bad obj values', () => { 50 | const allBrokenObj: Record = { 51 | borderStyle: 'invalid-border-style', 52 | borderLabel: undefined, 53 | borderLabelPosition: new Error(), 54 | previewBorderStyle: 10, 55 | padding: 0, 56 | margin: ',', 57 | prompt: null, 58 | marker: undefined, 59 | pointer: 2, 60 | separator: 1.04, 61 | scrollbar: new Error(), 62 | layout: new Date(), 63 | info: 'nothing to see here', 64 | }; 65 | 66 | const output = validateAndParseThemeOptions(allBrokenObj); 67 | 68 | expect(output).toEqual(initialOptions); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/components/common/InputCycle.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 | 37 | 44 | 47 | 50 | 51 | 52 | 96 | -------------------------------------------------------------------------------- /src/styles/terminal.css: -------------------------------------------------------------------------------- 1 | .terminal-window { 2 | span { 3 | display: inline-block; 4 | white-space: nowrap; 5 | } 6 | 7 | .fg { 8 | color: var(--fzf-fg); 9 | } 10 | 11 | .bg { 12 | background-color: var(--fzf-bg); 13 | } 14 | 15 | .hl { 16 | color: var(--fzf-hl); 17 | } 18 | 19 | .pointer { 20 | color: var(--fzf-pointer); 21 | } 22 | 23 | .marker { 24 | color: var(--fzf-marker); 25 | } 26 | 27 | .gutter { 28 | background-color: var(--fzf-gutter); 29 | } 30 | 31 | .header { 32 | color: var(--fzf-header); 33 | } 34 | 35 | .info { 36 | color: var(--fzf-info); 37 | } 38 | 39 | .spinner { 40 | color: var(--fzf-spinner); 41 | position: relative; 42 | 43 | &::after { 44 | position: absolute; 45 | left: 0; 46 | animation: terminal-spinner 0.8s linear infinite; 47 | display: inline; 48 | content: '⠋'; 49 | } 50 | } 51 | 52 | .prompt { 53 | color: var(--fzf-prompt); 54 | } 55 | 56 | .border { 57 | color: var(--fzf-border); 58 | } 59 | 60 | .separator { 61 | color: var(--fzf-separator); 62 | } 63 | 64 | .scrollbar { 65 | color: var(--fzf-scrollbar); 66 | } 67 | 68 | .bg-plus { 69 | background-color: var(--fzf-bg-plus); 70 | font-weight: bold; 71 | } 72 | 73 | .fg-plus { 74 | color: var(--fzf-fg-plus); 75 | } 76 | 77 | .hl-plus { 78 | color: var(--fzf-hl-plus); 79 | } 80 | 81 | .preview-fg { 82 | color: var(--fzf-preview-fg); 83 | } 84 | 85 | .preview-bg { 86 | background-color: var(--fzf-preview-bg); 87 | } 88 | 89 | .preview-border { 90 | color: var(--fzf-preview-border); 91 | } 92 | 93 | .preview-scrollbar { 94 | color: var(--fzf-preview-scrollbar); 95 | } 96 | 97 | .preview-label { 98 | color: var(--fzf-preview-labe); 99 | } 100 | 101 | .label { 102 | color: var(--fzf-label); 103 | } 104 | 105 | .query { 106 | color: var(--fzf-query); 107 | position: relative; 108 | 109 | &::after { 110 | content: '█'; 111 | color: var(--fzf-fg); 112 | animation: cursor-blink 1.5s steps(1) infinite; 113 | position: absolute; 114 | right: 0; 115 | transform: translateX(100%); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/components/common/InputWithHelp.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 | 21 | {@html tooltipContent} 22 |
23 | 24 | 92 | -------------------------------------------------------------------------------- /src/data/import/importFromUrlHash.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, describe } from 'vitest'; 2 | import { initialColors } from '~/data/colors.store'; 3 | import { encodeObjForUrlHash } from '~/data/export/exportToUrlHash'; 4 | import { importFromUrlHash } from '~/data/import/importFromUrlHash'; 5 | import { initialOptions } from '~/data/options.store'; 6 | 7 | const allWrongColors = { 8 | 'fg': undefined, 9 | 'fg-plus': undefined, 10 | 'bg': undefined, 11 | 'bg-plus': undefined, 12 | 'hl': undefined, 13 | 'hl-plus': undefined, 14 | 'marker': undefined, 15 | 'prompt': undefined, 16 | 'spinner': undefined, 17 | 'pointer': undefined, 18 | 'border': undefined, 19 | 'separator': 'GERALT', 20 | 'scrollbar': null, 21 | 'gutter': 42, 22 | 'query': undefined, 23 | 'disabled': [], 24 | 'preview-fg': new Date(), 25 | 'preview-bg': 1.22, 26 | 'preview-border': 'WAT', 27 | 'preview-scrollbar': Number.NaN, 28 | 'preview-label': new Error(), 29 | 'label': undefined, 30 | 'header': undefined, 31 | 'info': undefined, 32 | }; 33 | 34 | describe('importFromUrlHash()', () => { 35 | it('should handle malformed string', () => { 36 | const output = importFromUrlHash('this aint right...'); 37 | 38 | expect(output).toEqual({ 39 | themeOptions: initialOptions, 40 | colors: initialColors, 41 | }); 42 | }); 43 | 44 | it('should handle base 64 string that contains NO object', () => { 45 | const output = importFromUrlHash( 46 | 'aHR0cHM6Ly93d3cueW91dHViZS5jb20vd2F0Y2g/dj1kUXc0dzlXZ1hjUQ==', 47 | ); 48 | 49 | expect(output).toEqual({ 50 | themeOptions: initialOptions, 51 | colors: initialColors, 52 | }); 53 | }); 54 | 55 | it('should handle base 64 malformed object', () => { 56 | const str = encodeObjForUrlHash({ shouldnt: 'be here', why: undefined }); 57 | 58 | const output = importFromUrlHash(str); 59 | 60 | expect(output).toEqual({ 61 | themeOptions: initialOptions, 62 | colors: initialColors, 63 | }); 64 | }); 65 | 66 | it('can handle partially correct objects', () => { 67 | const str = encodeObjForUrlHash({ 68 | borderStyle: 'double', 69 | colors: allWrongColors, 70 | }); 71 | 72 | const output = importFromUrlHash(str); 73 | 74 | expect(output).toEqual({ 75 | themeOptions: { ...initialOptions, borderStyle: 'double' }, 76 | colors: initialColors, 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/data/export/exportToUrlHash.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, describe } from 'vitest'; 2 | 3 | import { initialOptions, type ThemeOptions } from '~/data/options.store'; 4 | import type { ColorValues } from '~/data/colors.store'; 5 | import { importFromUrlHash } from '~/data/import/importFromUrlHash'; 6 | import { exportToUrlHash } from '~/data/export/exportToUrlHash'; 7 | 8 | const sampleThemeOptions: ThemeOptions = { 9 | borderStyle: 'rounded', 10 | borderLabel: '', 11 | borderLabelPosition: 0, 12 | previewBorderStyle: 'rounded', 13 | padding: '0', 14 | margin: '0', 15 | prompt: '> ', 16 | marker: '>', 17 | pointer: '◆', 18 | separator: '─', 19 | scrollbar: '', 20 | layout: 'default', 21 | info: 'default', 22 | }; 23 | 24 | const sampleColorOptions: ColorValues = { 25 | 'fg': '#d0d0d0', 26 | 'fg-plus': '#d0d0d0', 27 | 'bg': '#121212', 28 | 'bg-plus': '#262626', 29 | 'hl': '#5f87af', 30 | 'hl-plus': '#5fd7ff', 31 | 'marker': '#87ff00', 32 | 'prompt': '#d7005f', 33 | 'spinner': '#af5fff', 34 | 'pointer': '#af5fff', 35 | 'border': '#262626', 36 | 'separator': '', 37 | 'scrollbar': '', 38 | 'gutter': '', 39 | 'query': '#d9d9d9', 40 | 'disabled': '', 41 | 'preview-fg': '', 42 | 'preview-bg': '', 43 | 'preview-border': '', 44 | 'preview-scrollbar': '', 45 | 'preview-label': '', 46 | 'label': '#aeaeae', 47 | 'header': '#87afaf', 48 | 'info': '#afaf87', 49 | }; 50 | 51 | describe('exportToUrlHash()', () => { 52 | it('should be able to encode and decode url hash to same object when exporting all options', () => { 53 | const urlOutput = exportToUrlHash(sampleThemeOptions, sampleColorOptions, false); 54 | 55 | const url = new URL(urlOutput); 56 | 57 | const imported = importFromUrlHash(url.hash.substring(1)); 58 | 59 | expect(imported).toEqual({ 60 | themeOptions: sampleThemeOptions, 61 | colors: sampleColorOptions, 62 | }); 63 | }); 64 | 65 | it('should be able to encode and decode url hash to same object when exporting colorsOnly', () => { 66 | const urlOutput = exportToUrlHash(sampleThemeOptions, sampleColorOptions, true); 67 | 68 | const url = new URL(urlOutput); 69 | 70 | const imported = importFromUrlHash(url.hash.substring(1)); 71 | 72 | expect(imported).toEqual({ 73 | themeOptions: initialOptions, // ignores provided options and uses default 74 | colors: sampleColorOptions, 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/components/common/Modal.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | (showModal = false)} 16 | on:click|self={() => dialog.close()} 17 | > 18 | 19 |
20 |
21 | 22 | 25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 | 96 | -------------------------------------------------------------------------------- /src/data/colors.store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | import { colorDefinitions, colorInheritances } from '~/fzf/fzfColorDefinitions'; 3 | import { colorsSchema, type ColorName } from '~/data/colors.schema'; 4 | 5 | export type ColorValues = Record; 6 | 7 | export type ColorOptions = { 8 | selectedColor: ColorName; 9 | colorPickerColor: string; 10 | colors: ColorValues; 11 | }; 12 | 13 | export const initialColors = colorsSchema.parse({}); 14 | 15 | const initialColorStore: ColorOptions = { 16 | selectedColor: 'fg', 17 | colorPickerColor: colorDefinitions.fg.initial, 18 | colors: initialColors, 19 | }; 20 | 21 | const _colorsStore = writable(initialColorStore); 22 | 23 | /** 24 | * Given a color token, get its value from the store, or recursively try 25 | * to find the first parent with a color. 26 | */ 27 | export const getColorOrFallback = (color: ColorName, currentColors: ColorValues) => { 28 | const thisColor = { color, value: currentColors[color] }; 29 | 30 | if (currentColors[color]) { 31 | return thisColor; 32 | } 33 | 34 | // given the inheritance tree, find the first that has a value 35 | const firstMatched = colorInheritances[color].find( 36 | (inheritedColor) => currentColors[inheritedColor], 37 | ); 38 | 39 | return firstMatched ? { color: firstMatched, value: currentColors[firstMatched] } : thisColor; 40 | }; 41 | 42 | export const isValidColor = (color: string): color is ColorName => { 43 | return color in colorDefinitions; 44 | }; 45 | 46 | export const colorsStore = { 47 | subscribe: _colorsStore.subscribe, 48 | setSelected: (token: ColorName) => { 49 | _colorsStore.update((settings) => ({ 50 | ...settings, 51 | selectedColor: token, 52 | colorPickerColor: settings.colors[token], 53 | })); 54 | }, 55 | resetAllColors: () => { 56 | _colorsStore.update((settings) => ({ 57 | ...initialColorStore, 58 | selectedColor: settings.selectedColor, 59 | colorPickerColor: colorDefinitions[settings.selectedColor].initial, 60 | })); 61 | }, 62 | updateColor: (token: ColorName, color: string | undefined) => { 63 | _colorsStore.update((settings) => ({ 64 | ...settings, 65 | colors: { 66 | ...settings.colors, 67 | [token]: color, 68 | }, 69 | })); 70 | }, 71 | updateAllColors: (values: ColorValues) => { 72 | _colorsStore.update((settings) => ({ 73 | ...settings, 74 | colors: { 75 | ...settings.colors, 76 | ...values, 77 | }, 78 | })); 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/components/ImportOptions.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | 40 |
41 | 42 |

Import Options

43 | 44 |
45 |

46 | Paste the contents of your FZF_DEFAULT_OPTS variable. 47 |

48 | 49 | 54 | 55 | 67 |
68 |
69 |
70 | 71 | 92 | -------------------------------------------------------------------------------- /src/utils/svelte/usePopper.ts: -------------------------------------------------------------------------------- 1 | import { createPopper } from '@popperjs/core'; 2 | import type { Instance, Options, StrictModifiers } from '@popperjs/core'; 3 | 4 | export default function createPopperAction() { 5 | let popperParams: Partial; 6 | let popperTriggerElement: Element | null; 7 | let popperTooltip: HTMLElement | null; 8 | let popperInstance: Instance | null; 9 | 10 | function show() { 11 | if (!popperTriggerElement || !popperTooltip) return; 12 | 13 | popperTooltip.classList.remove('hidden'); 14 | popperTooltip.setAttribute('data-show', ''); 15 | 16 | popperInstance = createPopper( 17 | popperTriggerElement, 18 | popperTooltip, 19 | popperParams, 20 | ); 21 | } 22 | 23 | function hide() { 24 | if (!popperTriggerElement || !popperTooltip) return; 25 | 26 | popperTooltip.removeAttribute('data-show'); 27 | popperTooltip.classList.add('hidden'); 28 | 29 | if (popperInstance) { 30 | popperInstance.destroy(); 31 | popperInstance = null; 32 | } 33 | } 34 | 35 | function initializePopper() { 36 | if (!popperTriggerElement || !popperTooltip) return; 37 | 38 | popperTriggerElement.addEventListener('mouseenter', show); 39 | popperTriggerElement.addEventListener('mouseleave', hide); 40 | } 41 | 42 | function destroyPopper() { 43 | if (!popperInstance) return; 44 | 45 | popperInstance.destroy(); 46 | popperInstance = null; 47 | } 48 | 49 | function usePopperTrigger(element: HTMLElement) { 50 | popperTriggerElement = element; 51 | initializePopper(); 52 | 53 | return { 54 | destroy() { 55 | popperTriggerElement?.removeEventListener('mouseenter', show); 56 | popperTriggerElement?.removeEventListener('mouseleave', hide); 57 | popperTriggerElement = null; 58 | destroyPopper(); 59 | }, 60 | }; 61 | } 62 | 63 | function usePopperTooltip(element: HTMLElement, params: Partial = {}) { 64 | popperTooltip = element; 65 | popperParams = { 66 | placement: 'bottom', 67 | modifiers: [ 68 | { name: 'offset', options: { offset: [0, 15] } }, 69 | { name: 'preventOverflow', options: { mainAxis: true } }, 70 | ], 71 | ...params, 72 | }; 73 | initializePopper(); 74 | 75 | popperTooltip.classList.add('hidden'); 76 | 77 | return { 78 | update(newParams: Partial) { 79 | popperParams = newParams; 80 | popperInstance?.setOptions(popperParams); 81 | }, 82 | destroy() { 83 | popperTooltip = null; 84 | destroyPopper(); 85 | }, 86 | }; 87 | } 88 | 89 | return [usePopperTrigger, usePopperTooltip]; 90 | } 91 | -------------------------------------------------------------------------------- /src/utils/tui/addBorders.ts: -------------------------------------------------------------------------------- 1 | import { BorderStyleDefinitions, type BorderStyle } from '~/fzf/fzfBorders'; 2 | import { Line } from '~/utils/tui/Line'; 3 | import { token, fillSpace, Token, FillSpace } from '~/utils/tui/Token'; 4 | 5 | type BorderOptions = { 6 | className?: string; 7 | style: BorderStyle; 8 | label: string; 9 | position: number; 10 | }; 11 | 12 | export const addBorders = (lines: Line[], border: BorderOptions) => { 13 | const borderDefinition = BorderStyleDefinitions[border.style || 'none']; 14 | 15 | const className = border.className || 'border'; 16 | 17 | if (!border.style || border.style === 'none') { 18 | return lines; 19 | } 20 | 21 | const isTop = true; 22 | 23 | let borderLabel: Array = [ 24 | fillSpace(borderDefinition[isTop ? 'top' : 'bottom'], className), 25 | ]; 26 | 27 | if (border.label) { 28 | const borderChar = borderDefinition[isTop ? 'top' : 'bottom']; 29 | 30 | if (border.position === 0) { 31 | // text-center 32 | borderLabel = [ 33 | fillSpace(borderChar, className), 34 | token(border.label, 'label'), 35 | fillSpace(borderChar, className), 36 | ]; 37 | } else if (border.position > 0) { 38 | // left-aligned 39 | const pos = border.position - 1; 40 | 41 | borderLabel = [ 42 | token(borderChar.repeat(pos), className), 43 | token(border.label, 'label'), 44 | fillSpace(borderChar, className), 45 | ]; 46 | } else if (border.position < 0) { 47 | // right-aligned 48 | const pos = Math.abs(border.position) - 1; 49 | 50 | borderLabel = [ 51 | fillSpace(borderChar, className), 52 | token(border.label, 'label'), 53 | token(borderChar.repeat(pos), className), 54 | ]; 55 | } 56 | } 57 | 58 | const linesWithBorder = [ 59 | new Line({ 60 | className, 61 | tokens: [ 62 | token(borderDefinition.topLeft, className), 63 | ...(isTop ? borderLabel : [fillSpace(borderDefinition.top, className)]), 64 | token(borderDefinition.topRight, className), 65 | ], 66 | }), 67 | ...lines.map((line) => { 68 | line.tokens.unshift(token(borderDefinition.left, className)); 69 | 70 | if (!line.fillSpaceCount()) { 71 | line.tokens.push(fillSpace(' ')); 72 | } 73 | 74 | line.tokens.push(token(borderDefinition.right, className)); 75 | 76 | return line; 77 | }), 78 | new Line({ 79 | className, 80 | tokens: [ 81 | token(borderDefinition.bottomLeft, className), 82 | fillSpace(borderDefinition.bottom, className), 83 | token(borderDefinition.bottomRight, className), 84 | ], 85 | }), 86 | ]; 87 | 88 | return linesWithBorder; 89 | }; 90 | -------------------------------------------------------------------------------- /src/data/export/exportToEnvVariable.ts: -------------------------------------------------------------------------------- 1 | import { isValidColor, type ColorValues } from '~/data/colors.store'; 2 | import { fzfOptionsConfig, type FzfOptionDefinition } from '~/data/fzfOptions.config'; 3 | import { isValidOption, type ThemeOptions } from '~/data/options.store'; 4 | import { colorDefinitions } from '~/fzf/fzfColorDefinitions'; 5 | import { arrayChunk } from '~/utils/arrayChunk'; 6 | import { toFzfColorName } from '~/utils/colors/toFzfColorName'; 7 | 8 | const sanitize = (str: string) => { 9 | return `"${str}"`; 10 | }; 11 | 12 | const prepareForEnvExport = (themeOptions: ThemeOptions, colors: ColorValues) => { 13 | const optionsVariables: Map = new Map(); 14 | const colorVariables: Map = new Map(); 15 | 16 | Object.entries(colors).forEach(([name, value]) => { 17 | if (!isValidColor(name)) return; 18 | 19 | const fzfColorName = toFzfColorName(name); 20 | const definitions = colorDefinitions[name]; 21 | 22 | if (value) { 23 | colorVariables.set(fzfColorName, value); 24 | } else if (definitions.nullable && !definitions.inherits) { 25 | colorVariables.set(fzfColorName, '-1'); 26 | } 27 | }); 28 | 29 | Object.keys(themeOptions).forEach((key) => { 30 | if (!isValidOption(key)) return; 31 | 32 | const conf = fzfOptionsConfig[key] as FzfOptionDefinition; 33 | const storeValue = themeOptions[key]; 34 | 35 | if (!conf) return; 36 | 37 | const formatted = conf.transformExport 38 | ? conf.transformExport(String(storeValue), themeOptions) 39 | : String(storeValue); 40 | 41 | // abort early if shouldn't export 42 | if (conf.exportIf && !conf.exportIf(formatted, themeOptions)) return; 43 | 44 | optionsVariables.set(conf.argName, formatted); 45 | }); 46 | 47 | return { optionsVariables, colorVariables }; 48 | }; 49 | 50 | export const exportToEnvVariable = ( 51 | themeOptions: ThemeOptions, 52 | colors: ColorValues, 53 | colorsOnly: boolean, 54 | ) => { 55 | const { colorVariables, optionsVariables } = prepareForEnvExport(themeOptions, colors); 56 | 57 | const colorsForEnv = [...colorVariables.keys()].map((color) => { 58 | return `${color}:${colorVariables.get(color)}`; 59 | }); 60 | 61 | const optionsForEnv = [...optionsVariables.keys()].map((option) => { 62 | return `--${option}=${sanitize(optionsVariables.get(option) ?? '""')}`; 63 | }); 64 | 65 | // split all colors into lines with max 4 colors each 66 | const colorChunks = arrayChunk(colorsForEnv, 4) 67 | .map((chunk) => `--color=${chunk.join(',')}`) 68 | .join('\n '); 69 | 70 | const optionsChunks = colorsOnly 71 | ? [] 72 | : arrayChunk(optionsForEnv, 4) 73 | .map((chunk) => chunk.join(' ')) 74 | .join('\n '); 75 | 76 | return `export FZF_DEFAULT_OPTS=$FZF_DEFAULT_OPTS'\n ${colorChunks}${ 77 | optionsChunks.length > 0 ? `\n ${optionsChunks}` : '' 78 | }'`; 79 | }; 80 | -------------------------------------------------------------------------------- /src/fzf/fzfBorders.ts: -------------------------------------------------------------------------------- 1 | export type BorderTypeGlyphs = { 2 | top: string; 3 | bottom: string; 4 | left: string; 5 | right: string; 6 | topLeft: string; 7 | topRight: string; 8 | bottomLeft: string; 9 | bottomRight: string; 10 | }; 11 | 12 | export type BorderStyle = 'rounded' | 'sharp' | 'bold' | 'double' | 'block' | 'thinblock' | 'none'; 13 | export type BorderStyleNonNullable = Exclude; 14 | 15 | // @todo: add border disclaimers for block and thinblock 16 | export const BorderStyleDefinitions: Record = { 17 | none: { 18 | top: '', 19 | bottom: '', 20 | left: '', 21 | right: '', 22 | topLeft: '', 23 | topRight: '', 24 | bottomLeft: '', 25 | bottomRight: '', 26 | }, 27 | // ╭─────────────────╮ 28 | // │ rounded │ 29 | // ╰─────────────────╯ 30 | rounded: { 31 | top: '─', 32 | bottom: '─', 33 | left: '│', 34 | right: '│', 35 | topLeft: '╭', 36 | topRight: '╮', 37 | bottomLeft: '╰', 38 | bottomRight: '╯', 39 | }, 40 | // ┌─────────────────┐ 41 | // │ sharp │ 42 | // └─────────────────┘ 43 | sharp: { 44 | top: '─', 45 | bottom: '─', 46 | left: '│', 47 | right: '│', 48 | topLeft: '┌', 49 | topRight: '┐', 50 | bottomLeft: '└', 51 | bottomRight: '┘', 52 | }, 53 | // ┏━━━━━━━━━━━━━━━━━┓ 54 | // ┃ bold ┃ 55 | // ┗━━━━━━━━━━━━━━━━━┛ 56 | bold: { 57 | top: '━', 58 | bottom: '━', 59 | left: '┃', 60 | right: '┃', 61 | topLeft: '┏', 62 | topRight: '┓', 63 | bottomLeft: '┗', 64 | bottomRight: '┛', 65 | }, 66 | // ╔════════════════╗ 67 | // ║ double ║ 68 | // ╚════════════════╝ 69 | double: { 70 | top: '═', 71 | bottom: '═', 72 | left: '║', 73 | right: '║', 74 | topLeft: '╔', 75 | topRight: '╗', 76 | bottomLeft: '╚', 77 | bottomRight: '╝', 78 | }, 79 | // ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ 80 | // ▌ block ▐ 81 | // ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟ 82 | block: { 83 | top: '▀', 84 | bottom: '▄', 85 | left: '▌', 86 | right: '▐', 87 | topLeft: '▛', 88 | topRight: '▜', 89 | bottomLeft: '▙', 90 | bottomRight: '▟', 91 | }, 92 | thinblock: { 93 | top: '▔', 94 | bottom: '▁', 95 | left: '▏', 96 | right: '▕', 97 | topLeft: '🭽', 98 | topRight: '🭾', 99 | bottomLeft: '🭼', 100 | bottomRight: '🭿', 101 | }, 102 | }; 103 | 104 | export const borderTypes = Object.keys(BorderStyleDefinitions) as BorderStyle[]; 105 | export const borderTypesNonNullable = borderTypes.filter((item) => item !== 'none'); 106 | 107 | export const layoutTypes = ['default' as const, 'reverse' as const, 'reverse-list' as const]; 108 | export type Layout = (typeof layoutTypes)[number]; 109 | 110 | // @todo: add more INFO options 111 | export const infoTypes = ['default' as const, 'right' as const]; 112 | export type InfoStyle = (typeof infoTypes)[number]; 113 | -------------------------------------------------------------------------------- /src/utils/tui/renderLines.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOptions } from '~/data/options.store'; 2 | import { stringToBoxCoordinates } from '~/utils/boxCoordinates'; 3 | import type { Line } from '~/utils/tui/Line'; 4 | import { addBorders } from '~/utils/tui/addBorders'; 5 | import { addSpacing } from '~/utils/tui/addSpacing'; 6 | import { createFinderLines } from '~/utils/tui/createFinderLines'; 7 | import { createPreviewLines } from '~/utils/tui/createPreviewLines'; 8 | import { mergeRenderedLines } from '~/utils/tui/mergeLines'; 9 | 10 | const countNeededHorizontalSpace = (theme: ThemeOptions) => { 11 | const marginCoords = stringToBoxCoordinates(theme.margin); 12 | const paddingCoords = stringToBoxCoordinates(theme.padding); 13 | 14 | const borderCount = theme.borderStyle === 'none' ? 0 : 2; 15 | const margin = marginCoords.left + marginCoords.right; 16 | const padding = paddingCoords.left + paddingCoords.right; 17 | 18 | return borderCount + margin + padding; 19 | }; 20 | 21 | const minColsNeededForLines = (lines: Line[]) => { 22 | return lines.reduce((count, line) => Math.max(count, line.staticContentLength()), 0); 23 | }; 24 | 25 | export const renderLines = (maxScreenCols: number, theme: ThemeOptions) => { 26 | let finderLines = createFinderLines(theme); 27 | let previewLines = createPreviewLines(theme); 28 | 29 | // calculate min space needed to render: ui, left column, right column 30 | const colsNeededForUi = countNeededHorizontalSpace(theme); 31 | const minimumLinesLeft = minColsNeededForLines(finderLines); 32 | const minimumLinesRight = minColsNeededForLines(previewLines); 33 | 34 | // total size we need to render all text + all ui elements configured 35 | const minStaticContent = minimumLinesLeft + minimumLinesRight + colsNeededForUi; 36 | 37 | // calculate how much "empty" space to give to each side, given the available screen size 38 | const totalEmptySpace = 39 | maxScreenCols - minStaticContent > 0 ? maxScreenCols - minStaticContent : 0; 40 | const emptySpaceLeft = minimumLinesLeft + Math.floor(totalEmptySpace / 2) + (totalEmptySpace % 2); 41 | const emptySpaceRight = minimumLinesRight + Math.floor(totalEmptySpace / 2); 42 | 43 | const colsToRender = Math.max(maxScreenCols, minStaticContent); 44 | 45 | finderLines.forEach((line) => void line.computeFillSpace(emptySpaceLeft)); 46 | previewLines.forEach((line) => void line.computeFillSpace(emptySpaceRight)); 47 | 48 | let mergedLines = mergeRenderedLines(finderLines, previewLines); 49 | 50 | mergedLines = addSpacing(mergedLines, stringToBoxCoordinates(theme.padding), 'bg outer-spacing'); 51 | mergedLines = addBorders(mergedLines, { 52 | style: theme.borderStyle, 53 | label: theme.borderLabel?.substring(0, colsToRender - 2) ?? '', 54 | position: theme.borderLabelPosition, 55 | className: 'bg border', 56 | }); 57 | mergedLines = addSpacing(mergedLines, stringToBoxCoordinates(theme.margin), 'outer-spacing'); 58 | 59 | return mergedLines.map((line) => { 60 | line.computeFillSpace(colsToRender); 61 | return line.render(); 62 | }); 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/ColorPicker.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 |
32 | {#if colorDefinitions[$colorsStore.selectedColor].nullable && $colorsStore.colors[$colorsStore.selectedColor]} 33 |
34 | 44 |
45 | {/if} 46 | 47 | { 53 | const newColor = event.detail.hex; 54 | colorsStore.updateColor($colorsStore.selectedColor, newColor); 55 | }} 56 | --picker-radius="0" 57 | --slider-width="15px" 58 | --picker-width="280px" 59 | --picker-height={pickerHeight} 60 | /> 61 | 62 |
63 | {toFzfColorName($colorsStore.selectedColor)} 64 | 65 | {#if !$colorsStore.colors[$colorsStore.selectedColor] && !inheritsFrom[0]} 66 | (use terminal default) 67 | {:else if inheritsFrom.length > 0} 68 | ({'inherits from '}{toFzfColorName(inheritsFrom[0])}) 69 | {/if} 70 |
71 |
72 | 73 | 100 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-engines.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-engines", 5 | factory: function (require) { 6 | var plugin=(()=>{var P=Object.create,f=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var j=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty;var b=n=>f(n,"__esModule",{value:!0});var i=n=>{if(typeof require!="undefined")return require(n);throw new Error('Dynamic require of "'+n+'" is not supported')};var T=(n,e)=>{for(var r in e)f(n,r,{get:e[r],enumerable:!0})},V=(n,e,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of N(e))!Y.call(n,t)&&t!=="default"&&f(n,t,{get:()=>e[t],enumerable:!(r=R(e,t))||r.enumerable});return n},s=n=>V(b(f(n!=null?P(j(n)):{},"default",n&&n.__esModule&&"default"in n?{get:()=>n.default,enumerable:!0}:{value:n,enumerable:!0})),n);var U={};T(U,{default:()=>q});var o=s(i("@yarnpkg/core")),c;(function(r){r.Yarn="Yarn",r.Console="Console"})(c||(c={}));var h=class{constructor(e){this.throwWrongEngineError=(e,r)=>{let t=this.formatErrorMessage(e,r);this.throwError(t)};this.throwError=e=>{switch(this.errorReporter){case c.Yarn:this.reportYarnError(e);break;case c.Console:default:this.reportConsoleError(e);break}};this.reportYarnError=e=>{throw new o.ReportError(o.MessageName.UNNAMED,e)};this.reportConsoleError=e=>{console.error(e),process.exit(1)};this.formatErrorMessage=(e,r)=>{let{configuration:t}=this.project,p=o.formatUtils.applyStyle(t,o.formatUtils.pretty(t,this.engine,"green"),2),g=o.formatUtils.pretty(t,e,"cyan"),d=o.formatUtils.pretty(t,r,"cyan"),w=`The current ${p} version ${g} does not satisfy the required version ${d}.`;return o.formatUtils.pretty(t,w,"red")};this.project=e.project,this.errorReporter=e.errorReporter}};var m=s(i("fs")),y=s(i("path")),l=s(i("semver")),k=s(i("@yarnpkg/fslib")),a=s(i("@yarnpkg/core"));var v=class extends h{constructor(){super(...arguments);this.resolveNvmRequiredVersion=()=>{let{configuration:e,cwd:r}=this.project,t=(0,y.resolve)(k.npath.fromPortablePath(r),".nvmrc"),p=a.formatUtils.applyStyle(e,a.formatUtils.pretty(e,this.engine,"green"),2);if(!(0,m.existsSync)(t)){this.throwError(a.formatUtils.pretty(e,`Unable to verify the ${p} version. The .nvmrc file does not exist.`,"red"));return}let g=(0,m.readFileSync)(t,"utf-8").trim();if((0,l.validRange)(g))return g;let d=a.formatUtils.pretty(e,".nvmrc","yellow");this.throwError(a.formatUtils.pretty(e,`Unable to verify the ${p} version. The ${d} file contains an invalid semver range.`,"red"))}}get engine(){return"Node"}verifyEngine(e){let r=e.node;r!=null&&(r===".nvmrc"&&(r=this.resolveNvmRequiredVersion()),(0,l.satisfies)(process.version,r,{includePrerelease:!0})||this.throwWrongEngineError(process.version.replace(/^v/i,""),r.replace(/^v/i,"")))}};var x=s(i("semver")),E=s(i("@yarnpkg/core"));var u=class extends h{get engine(){return"Yarn"}verifyEngine(e){let r=e.yarn;r!=null&&((0,x.satisfies)(E.YarnVersion,r,{includePrerelease:!0})||this.throwWrongEngineError(E.YarnVersion,r))}};var C=n=>e=>{if(process.env.PLUGIN_YARN_ENGINES_DISABLE!=null)return;let{engines:r={}}=e.getWorkspaceByCwd(e.cwd).manifest.raw,t={project:e,errorReporter:n};[new v(t),new u(t)].forEach(g=>g.verifyEngine(r))},S={hooks:{validateProject:C(c.Yarn),setupScriptEnvironment:C(c.Console)}},q=S;return U;})(); 7 | return plugin; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/tui/Line.ts: -------------------------------------------------------------------------------- 1 | import { Token, FillSpace, token } from '~/utils/tui/Token'; 2 | 3 | type LineToken = undefined | string | Token | FillSpace; 4 | 5 | export type LineOptions = { 6 | className?: string; 7 | tokens: LineToken[]; 8 | }; 9 | 10 | export class Line { 11 | public tokens: LineToken[]; 12 | public className: string | undefined; 13 | 14 | constructor(options: LineOptions) { 15 | this.tokens = options.tokens.map((token) => { 16 | if (token instanceof Token) { 17 | token.addClass(options.className); 18 | } 19 | 20 | return token; 21 | }); 22 | 23 | this.className = options.className; 24 | } 25 | 26 | public clone(withClass = true) { 27 | return new Line({ 28 | className: withClass ? this.className : undefined, 29 | tokens: [...this.tokens], 30 | }); 31 | } 32 | 33 | public fillSpaceCount(): number { 34 | return this.tokens.filter((item) => item instanceof FillSpace).length; 35 | } 36 | 37 | public ensureContainsFillSpace() { 38 | if (!this.fillSpaceCount()) { 39 | this.tokens = [...this.tokens, new FillSpace(' ', this.className)]; 40 | } 41 | } 42 | 43 | public staticContentLength(): number { 44 | return this.tokens.reduce((length, item) => { 45 | if (typeof item === 'string') { 46 | return length + item.length; 47 | } else if (item instanceof Token) { 48 | return length + item.text.length; 49 | } 50 | 51 | return length; 52 | }, 0); 53 | } 54 | 55 | /** 56 | * This removes turns any FillSpace and replace them with static string tokens 57 | * that fills N columns 58 | */ 59 | computeFillSpace(cols: number) { 60 | this.ensureContainsFillSpace(); 61 | const fillSpaceCount = this.fillSpaceCount(); 62 | const initialFillSpaceChars = cols - this.staticContentLength(); 63 | 64 | let baseFillSpaceLength = Math.floor(initialFillSpaceChars / fillSpaceCount); 65 | let fillSpaceRemainder = initialFillSpaceChars % fillSpaceCount; 66 | 67 | const finalTokens: LineToken[] = this.tokens.map((item) => { 68 | if (item instanceof FillSpace && baseFillSpaceLength > 0) { 69 | let currentFillSpaceLength = baseFillSpaceLength; 70 | 71 | if (fillSpaceRemainder > 0) { 72 | currentFillSpaceLength++; 73 | fillSpaceRemainder--; 74 | } 75 | 76 | const fillString = item.fillChar.repeat(currentFillSpaceLength); 77 | 78 | return token(fillString.substring(0, currentFillSpaceLength), item.classNames).addClass( 79 | this.className, 80 | ); 81 | } 82 | 83 | // non FillSpace tokens are copied verbatim 84 | return item; 85 | }); 86 | 87 | this.tokens = finalTokens; 88 | } 89 | 90 | render() { 91 | const lineElement = document.createElement('div'); 92 | 93 | if (this.className) { 94 | lineElement.className = this.className; 95 | } 96 | 97 | this.tokens.forEach((item) => { 98 | if (!item) return; 99 | 100 | if (typeof item === 'string') { 101 | lineElement.appendChild(document.createTextNode(item)); 102 | } else if (item instanceof Token) { 103 | lineElement.appendChild(item.render(this.className)); 104 | } 105 | }); 106 | 107 | return lineElement; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/fzf/fzfColorDefinitions.ts: -------------------------------------------------------------------------------- 1 | import type { ColorName } from '~/data/colors.schema'; 2 | 3 | type ColorDefinition = { 4 | initial: string; 5 | inherits?: ColorName; 6 | nullable: boolean; 7 | }; 8 | 9 | export const colorDefinitions: Record = { 10 | 'fg': { 11 | initial: '#d0d0d0', 12 | nullable: true, 13 | }, 14 | 'fg-plus': { 15 | initial: '#d0d0d0', 16 | nullable: false, 17 | }, 18 | 'bg': { 19 | initial: '#121212', 20 | nullable: true, 21 | }, 22 | 'bg-plus': { 23 | initial: '#262626', 24 | nullable: false, 25 | }, 26 | 'hl': { 27 | initial: '#5f87af', 28 | nullable: false, 29 | }, 30 | 'hl-plus': { 31 | initial: '#5fd7ff', 32 | nullable: false, 33 | inherits: 'fg', 34 | }, 35 | 'marker': { 36 | initial: '#87ff00', 37 | nullable: false, 38 | }, 39 | 'prompt': { 40 | initial: '#d7005f', 41 | nullable: false, 42 | }, 43 | 'spinner': { 44 | initial: '#af5fff', 45 | nullable: false, 46 | }, 47 | 'pointer': { 48 | initial: '#af5fff', 49 | nullable: false, 50 | }, 51 | 'border': { 52 | initial: '#262626', 53 | nullable: false, 54 | }, 55 | 'separator': { 56 | initial: '', 57 | nullable: true, 58 | inherits: 'border', 59 | }, 60 | 'scrollbar': { 61 | initial: '', 62 | nullable: true, 63 | inherits: 'border', 64 | }, 65 | 'gutter': { 66 | initial: '', 67 | nullable: true, 68 | inherits: 'bg-plus', 69 | }, 70 | 'query': { 71 | initial: '#d9d9d9', 72 | nullable: false, 73 | }, 74 | 'disabled': { 75 | initial: '', 76 | nullable: true, 77 | inherits: 'query', 78 | }, 79 | 'preview-fg': { 80 | initial: '', 81 | nullable: true, 82 | inherits: 'fg', 83 | }, 84 | 'preview-bg': { 85 | initial: '', 86 | nullable: true, 87 | inherits: 'bg', 88 | }, 89 | 'preview-border': { 90 | initial: '', 91 | nullable: true, 92 | inherits: 'border', 93 | }, 94 | 'preview-scrollbar': { 95 | initial: '', 96 | nullable: true, 97 | inherits: 'border', 98 | }, 99 | 'preview-label': { 100 | initial: '', 101 | nullable: true, 102 | inherits: 'label', 103 | }, 104 | 'label': { 105 | initial: '#aeaeae', 106 | nullable: false, 107 | }, 108 | 'header': { 109 | initial: '#87afaf', 110 | nullable: false, 111 | }, 112 | 'info': { 113 | initial: '#afaf87', 114 | nullable: false, 115 | }, 116 | } as const; 117 | 118 | /** 119 | * once at runtime, create an easy dictionary of which color inherits from which 120 | */ 121 | export const colorInheritances = Object.fromEntries( 122 | Object.entries(colorDefinitions).map(([k, v]) => { 123 | let inherits: ColorName[] = []; 124 | 125 | let tokenLookup = v.inherits; 126 | 127 | while (tokenLookup) { 128 | inherits.push(tokenLookup); 129 | 130 | tokenLookup = colorDefinitions[tokenLookup].inherits; 131 | } 132 | 133 | return [k, inherits]; 134 | }), 135 | ) as Record; 136 | 137 | /** 138 | * The order of colors is constantly changing in the settings store, so prefer 139 | * to use {@link colorDefinitions} to guarantee consistent order 140 | */ 141 | export const orderedColorTokens = Object.keys(colorDefinitions) as ColorName[]; 142 | -------------------------------------------------------------------------------- /src/components/PaletteColor.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 |
40 | 53 |
54 | 55 | 126 | -------------------------------------------------------------------------------- /src/utils/tui/createFinderLines.ts: -------------------------------------------------------------------------------- 1 | import type { ThemeOptions } from '~/data/options.store'; 2 | import { Line } from '~/utils/tui/Line'; 3 | import { token, fillSpace } from '~/utils/tui/Token'; 4 | 5 | const addScrollbarToLines = (count: number, lines: Line[], themeOptions: ThemeOptions) => { 6 | lines.forEach((line, i) => { 7 | if (i >= count) return; 8 | 9 | line.tokens.push(fillSpace(' '), token(themeOptions.scrollbar, 'bg scrollbar')); 10 | }); 11 | 12 | return lines; 13 | }; 14 | 15 | export const createFinderLines = (themeOptions: ThemeOptions) => { 16 | const fileResultLines = [ 17 | new Line({ 18 | className: 'bg', 19 | tokens: [ 20 | token(' '.repeat(themeOptions.pointer.length), 'gutter'), 21 | token(themeOptions.marker, 'gutter marker'), 22 | token('src/fzf/tui/borders', 'fg'), 23 | token('.go', 'hl'), 24 | ], 25 | }), 26 | new Line({ 27 | className: 'bg', 28 | tokens: [ 29 | token(' '.repeat(themeOptions.pointer.length), 'gutter'), 30 | token(' '.repeat(themeOptions.marker.length)), 31 | token('src/fzf/tui/main', 'fg'), 32 | token('.go', 'hl'), 33 | ], 34 | }), 35 | new Line({ 36 | className: 'bg', 37 | tokens: [ 38 | token(' '.repeat(themeOptions.pointer.length), 'gutter'), 39 | token(themeOptions.marker, 'gutter marker'), 40 | token('src/options', 'fg'), 41 | token('.go', 'hl'), 42 | ], 43 | }), 44 | new Line({ 45 | className: 'bg', 46 | tokens: [ 47 | token(' '.repeat(themeOptions.pointer.length), 'gutter'), 48 | token(' '.repeat(themeOptions.marker.length)), 49 | token('src/matcher', 'fg'), 50 | token('.go', 'hl'), 51 | ], 52 | }), 53 | new Line({ 54 | className: 'bg', 55 | tokens: [ 56 | token(themeOptions.pointer, 'pointer bg-plus'), 57 | token(' '.repeat(themeOptions.marker.length), 'bg-plus'), 58 | token('src/history', 'fg-plus bg-plus'), 59 | token('.go', 'hl-plus bg-plus'), 60 | ], 61 | }), 62 | new Line({ 63 | className: 'bg', 64 | tokens: [ 65 | token(' '.repeat(themeOptions.pointer.length), 'gutter'), 66 | token(' '.repeat(themeOptions.marker.length)), 67 | token('src/reader', 'fg'), 68 | token('.go', 'hl'), 69 | ], 70 | }), 71 | new Line({ 72 | className: 'bg', 73 | tokens: [ 74 | token(' '.repeat(themeOptions.pointer.length), 'gutter'), 75 | token(' '.repeat(themeOptions.marker.length)), 76 | token('src/merger', 'fg'), 77 | token('.go', 'hl'), 78 | ], 79 | }), 80 | ]; 81 | 82 | const uiLines = [ 83 | new Line({ className: 'bg', tokens: [token(' '), token('This is Header', 'header')] }), 84 | new Line({ 85 | className: 'bg', 86 | tokens: [ 87 | token(' ', 'spinner'), 88 | token(' '), 89 | themeOptions.info === 'default' ? token('35/63 (3) ', 'info') : undefined, 90 | fillSpace(themeOptions.separator || ' ', 'separator'), 91 | themeOptions.info === 'right' ? token(' 35/63 (3)', 'info') : undefined, 92 | token(' '), 93 | ], 94 | }), 95 | new Line({ 96 | className: 'bg', 97 | tokens: [token(themeOptions.prompt, 'prompt'), token('.go$', 'query')], 98 | }), 99 | ]; 100 | 101 | switch (themeOptions.layout) { 102 | case 'default': // files first, ui after 103 | uiLines.unshift(...addScrollbarToLines(3, fileResultLines, themeOptions)); 104 | break; 105 | case 'reverse': // ui first, reversed files after 106 | uiLines.reverse(); 107 | uiLines.push(...addScrollbarToLines(3, fileResultLines.toReversed(), themeOptions)); 108 | break; 109 | case 'reverse-list': // reversed files first, ui after 110 | uiLines.unshift(...addScrollbarToLines(3, fileResultLines.toReversed(), themeOptions)); 111 | break; 112 | default: 113 | throw new Error(`Unsupported layout option: ${themeOptions.layout}`); 114 | } 115 | 116 | return uiLines; 117 | }; 118 | -------------------------------------------------------------------------------- /src/components/ExportOptions.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | 51 |
52 | 53 |

Export & Share

54 | 55 |
56 |
57 | 58 | 63 | 68 |
69 |
70 |

Permalink

71 |
72 | 79 |
80 | 83 |
84 | 85 |

Variable Export

86 | 87 |
88 | 89 | 101 |
102 |
103 | 104 | 136 | -------------------------------------------------------------------------------- /src/components/Home.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 |
49 |
50 | 51 |
52 | 53 |
54 | 55 | 56 | 57 |
58 | 59 |
60 | 61 | 62 | 63 |
64 | 65 |
66 | 67 | 68 | 69 |
70 | 71 |
72 | 73 |
74 |
75 | 76 | 88 | 89 | 167 | -------------------------------------------------------------------------------- /src/components/CustomColorPicker/TextInput.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 |
47 | {#if textInputModes.includes('hex')} 48 |
49 | 50 |
51 | {/if} 52 | 53 | {#if textInputModes.includes('rgb')} 54 |
55 |
56 |
R
57 | 65 |
66 |
67 |
G
68 | 76 |
77 |
78 |
B
79 | 87 |
88 |
89 | {/if} 90 | 91 | {#if textInputModes.includes('hsv')} 92 |
93 |
94 |
H
95 | 103 |
104 |
105 |
S
106 | 114 |
115 |
116 |
V
117 | 125 |
126 |
127 | {/if} 128 |
129 | 130 | 173 | -------------------------------------------------------------------------------- /src/data/import/importFromEnvArgs.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, describe } from 'vitest'; 2 | import { type ColorValues } from '~/data/colors.store'; 3 | import { importFromEnvArgs } from '~/data/import/importFromEnvArgs'; 4 | 5 | import { initialOptions, type ThemeOptions } from '~/data/options.store'; 6 | 7 | const sampleOptions: ThemeOptions = { 8 | borderStyle: 'block', 9 | borderLabel: ' Geralt of Rivia ', 10 | borderLabelPosition: 10, 11 | previewBorderStyle: 'double', 12 | padding: '2', 13 | margin: '3', 14 | prompt: 'Here: ', 15 | marker: '->', 16 | pointer: '*', 17 | separator: 'LALA ', 18 | scrollbar: '+', 19 | layout: 'reverse', 20 | info: 'right', 21 | }; 22 | 23 | const sampleColors: ColorValues = { 24 | 'fg': '#3d3131', 25 | 'fg-plus': '#d0d0d0', 26 | 'bg': '#ffdfdf', 27 | 'bg-plus': '#262626', 28 | 'hl': '#b0b05f', 29 | 'hl-plus': '#5fd7ff', 30 | 'info': '#054545', 31 | 'marker': '#87ffe5', 32 | 'prompt': '#1500d6', 33 | 'spinner': '#b60000', 34 | 'pointer': '#5ebcff', 35 | 'header': '#00ff66', 36 | 'gutter': '#1d350f', 37 | 'border': '#2200ff', 38 | 'separator': '#9e5757', 39 | 'scrollbar': '#ff00d0', 40 | 'preview-fg': '#b2c602', 41 | 'preview-bg': '#002309', 42 | 'preview-border': '#9b2f2f', 43 | 'preview-scrollbar': '#ffe100', 44 | 'preview-label': '#4f1212', 45 | 'label': '#ff93e9', 46 | 'query': '#ff0000', 47 | 'disabled': '#9a0000', 48 | }; 49 | 50 | describe('importFromEnvArgs()', () => { 51 | it('should parse full cli args', () => { 52 | const output = importFromEnvArgs(` 53 | --color=fg:#3d3131,fg+:#5f3952,bg:#ffdfdf,bg+:#af7272 54 | --color=hl:#b0b05f,hl+:#328c1b,info:#054545,marker:#87ffe5 55 | --color=prompt:#1500d6,spinner:#b60000,pointer:#5ebcff,header:#00ff66 56 | --color=gutter:#1d350f,border:#2200ff,separator:#9e5757,scrollbar:#ff00d0 57 | --color=preview-fg:#b2c602,preview-bg:#002309,preview-border:#9b2f2f,preview-scrollbar:#ffe100 58 | --color=preview-label:#4f1212,label:#ff93e9,query:#ff0000,disabled:#9a0000 59 | --border="block" --border-label=" Geralt of Rivia " --border-label-pos="10" --preview-window="border-double" 60 | --margin="3" --padding="2" --prompt="Here: " --pointer="*" 61 | --marker="->" --separator="LALA " --scrollbar="+" --layout="reverse" 62 | --info="right" 63 | `); 64 | 65 | expect(output).toEqual({ 66 | themeOptions: sampleOptions, 67 | colors: sampleColors, 68 | }); 69 | }); 70 | 71 | it('should parse colors-only obj', () => { 72 | const output = importFromEnvArgs(` 73 | --color=fg:#3d3131,fg+:#5f3952,bg:#ffdfdf,bg+:#af7272 74 | --color=hl:#b0b05f,hl+:#328c1b,info:#054545,marker:#87ffe5 75 | --color=prompt:#1500d6,spinner:#b60000,pointer:#5ebcff,header:#00ff66 76 | --color=gutter:#1d350f,border:#2200ff,separator:#9e5757,scrollbar:#ff00d0 77 | --color=preview-fg:#b2c602,preview-bg:#002309,preview-border:#9b2f2f,preview-scrollbar:#ffe100 78 | --color=preview-label:#4f1212,label:#ff93e9,query:#ff0000,disabled:#9a0000 79 | `); 80 | 81 | expect(output).toEqual({ 82 | themeOptions: initialOptions, // will use default 83 | colors: sampleColors, 84 | }); 85 | }); 86 | 87 | it('should be able to parse unset colors', () => { 88 | const output = importFromEnvArgs(` 89 | --color=fg:-1,bg:-1 90 | --color=fg+:#5f3952,,bg+:#af7272 91 | --color=hl:#b0b05f,hl+:#328c1b,info:#054545,marker:#87ffe5 92 | --color=prompt:#1500d6,spinner:#b60000,pointer:#5ebcff,header:#00ff66 93 | --color=gutter:#1d350f,border:#2200ff,separator:#9e5757,scrollbar:#ff00d0 94 | --color=preview-fg:#b2c602,preview-bg:#002309,preview-border:#9b2f2f,preview-scrollbar:#ffe100 95 | --color=preview-label:#4f1212,label:#ff93e9,query:#ff0000,disabled:#9a0000 96 | `); 97 | 98 | expect(output).toEqual({ 99 | themeOptions: initialOptions, // will use default 100 | colors: { 101 | ...sampleColors, 102 | fg: '', 103 | bg: '', 104 | }, 105 | }); 106 | }); 107 | 108 | it('should be able to parse empty colors', () => { 109 | // gutter is missing: 110 | const output = importFromEnvArgs(` 111 | --color=fg:#3d3131,fg+:#5f3952,bg:#ffdfdf,bg+:#af7272 112 | --color=hl:#b0b05f,hl+:#328c1b,info:#054545,marker:#87ffe5 113 | --color=prompt:#1500d6,spinner:#b60000,pointer:#5ebcff,header:#00ff66 114 | --color=border:#2200ff,separator:#9e5757,scrollbar:#ff00d0 115 | --color=preview-fg:#b2c602,preview-bg:#002309,preview-border:#9b2f2f,preview-scrollbar:#ffe100 116 | --color=preview-label:#4f1212,label:#ff93e9,query:#ff0000,disabled:#9a0000 117 | `); 118 | 119 | expect(output).toEqual({ 120 | themeOptions: initialOptions, 121 | colors: { 122 | ...sampleColors, 123 | gutter: '', 124 | }, 125 | }); 126 | }); 127 | 128 | it('should recover from wrong option values', () => { 129 | const output = importFromEnvArgs(` 130 | --color=fg:#3d3131,fg+:#5f3952,bg:#ffdfdf,bg+:#af7272 131 | --color=hl:#b0b05f,hl+:#328c1b,info:#054545,marker:#87ffe5 132 | --color=prompt:#1500d6,spinner:#b60000,pointer:#5ebcff,header:#00ff66 133 | --color=gutter:#1d350f,border:#2200ff,separator:#9e5757,scrollbar:#ff00d0 134 | --color=preview-fg:#b2c602,preview-bg:#002309,preview-border:#9b2f2f,preview-scrollbar:#ffe100 135 | --color=preview-label:#4f1212,label:#ff93e9,query:#ff0000,disabled:#9a0000 136 | --border="WHAT" --border-label-pos="THIS AINT RIGHT" --preview-window="NOPE" 137 | --margin="3,3,3,3,3" --padding="2,2,2,2,2,2,2,2" --pointer="TOOLONG" 138 | --marker="TOOLONG" --scrollbar="TOOLONG" --layout="WAT" 139 | --info="WAT" 140 | `); 141 | 142 | expect(output).toEqual({ 143 | themeOptions: initialOptions, 144 | colors: sampleColors, 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /src/components/OptionsPanel.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 |
18 |
Border
19 | 20 | 21 | { 26 | optionsStore.set('borderStyle', e.detail); 27 | }} 28 | /> 29 | 30 | 31 | 32 | void optionsStore.set('borderLabel', e.currentTarget.value)} 38 | /> 39 | 40 | 41 | 42 | 43 | 49 | void optionsStore.set( 50 | 'borderLabelPosition', 51 | Number.parseInt(e.currentTarget.value, 10), 52 | )} 53 | /> 54 | 55 | 56 | 57 | 58 | 59 | void optionsStore.set('separator', e.currentTarget.value)} 63 | /> 64 | 65 | 66 | 67 | { 72 | optionsStore.set('previewBorderStyle', e.detail); 73 | }} 74 | /> 75 | 76 |
77 | 78 |
79 |
Box
80 | 81 | 82 | { 87 | optionsStore.set('layout', e.detail); 88 | }} 89 | /> 90 | 91 | 92 | 93 | 94 | { 101 | if (e.currentTarget.checkValidity()) { 102 | optionsStore.set('margin', e.currentTarget.value); 103 | } 104 | }} 105 | /> 106 | 107 | 108 | 109 | 110 | 111 | 112 | { 118 | if (e.currentTarget.checkValidity()) { 119 | optionsStore.set('padding', e.currentTarget.value); 120 | } 121 | }} 122 | /> 123 | 124 | 125 | 126 | 127 | 128 | void optionsStore.set('scrollbar', e.currentTarget.value)} 133 | /> 134 | 135 |
136 | 137 |
138 |
Indicators
139 | 140 | 141 | void optionsStore.set('prompt', e.currentTarget.value)} 145 | /> 146 | 147 | 148 | 149 | void optionsStore.set('pointer', e.currentTarget.value)} 155 | /> 156 | 157 | 158 | 159 | void optionsStore.set('marker', e.currentTarget.value)} 165 | /> 166 | 167 | 168 | 169 | { 174 | optionsStore.set('info', e.detail); 175 | }} 176 | /> 177 | 178 |
179 |
180 | 181 | 220 | -------------------------------------------------------------------------------- /src/styles/reset.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { 178 | /* 1 */ 179 | overflow: visible; 180 | } 181 | 182 | /** 183 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 184 | * 1. Remove the inheritance of text transform in Firefox. 185 | */ 186 | 187 | button, 188 | select { 189 | /* 1 */ 190 | text-transform: none; 191 | } 192 | 193 | /** 194 | * Correct the inability to style clickable types in iOS and Safari. 195 | */ 196 | 197 | button, 198 | [type='button'], 199 | [type='reset'], 200 | [type='submit'] { 201 | -webkit-appearance: button; 202 | } 203 | 204 | /** 205 | * Remove the inner border and padding in Firefox. 206 | */ 207 | 208 | button::-moz-focus-inner, 209 | [type='button']::-moz-focus-inner, 210 | [type='reset']::-moz-focus-inner, 211 | [type='submit']::-moz-focus-inner { 212 | border-style: none; 213 | padding: 0; 214 | } 215 | 216 | /** 217 | * Restore the focus styles unset by the previous rule. 218 | */ 219 | 220 | button:-moz-focusring, 221 | [type='button']:-moz-focusring, 222 | [type='reset']:-moz-focusring, 223 | [type='submit']:-moz-focusring { 224 | outline: 1px dotted ButtonText; 225 | } 226 | 227 | /** 228 | * Correct the padding in Firefox. 229 | */ 230 | 231 | fieldset { 232 | padding: 0.35em 0.75em 0.625em; 233 | } 234 | 235 | /** 236 | * 1. Correct the text wrapping in Edge and IE. 237 | * 2. Correct the color inheritance from `fieldset` elements in IE. 238 | * 3. Remove the padding so developers are not caught out when they zero out 239 | * `fieldset` elements in all browsers. 240 | */ 241 | 242 | legend { 243 | box-sizing: border-box; /* 1 */ 244 | color: inherit; /* 2 */ 245 | display: table; /* 1 */ 246 | max-width: 100%; /* 1 */ 247 | padding: 0; /* 3 */ 248 | white-space: normal; /* 1 */ 249 | } 250 | 251 | /** 252 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 253 | */ 254 | 255 | progress { 256 | vertical-align: baseline; 257 | } 258 | 259 | /** 260 | * Remove the default vertical scrollbar in IE 10+. 261 | */ 262 | 263 | textarea { 264 | overflow: auto; 265 | } 266 | 267 | /** 268 | * 1. Add the correct box sizing in IE 10. 269 | * 2. Remove the padding in IE 10. 270 | */ 271 | 272 | [type='checkbox'], 273 | [type='radio'] { 274 | box-sizing: border-box; /* 1 */ 275 | padding: 0; /* 2 */ 276 | } 277 | 278 | /** 279 | * Correct the cursor style of increment and decrement buttons in Chrome. 280 | */ 281 | 282 | [type='number']::-webkit-inner-spin-button, 283 | [type='number']::-webkit-outer-spin-button { 284 | height: auto; 285 | } 286 | 287 | /** 288 | * 1. Correct the odd appearance in Chrome and Safari. 289 | * 2. Correct the outline style in Safari. 290 | */ 291 | 292 | [type='search'] { 293 | -webkit-appearance: textfield; /* 1 */ 294 | outline-offset: -2px; /* 2 */ 295 | } 296 | 297 | /** 298 | * Remove the inner padding in Chrome and Safari on macOS. 299 | */ 300 | 301 | [type='search']::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | /** 306 | * 1. Correct the inability to style clickable types in iOS and Safari. 307 | * 2. Change font properties to `inherit` in Safari. 308 | */ 309 | 310 | ::-webkit-file-upload-button { 311 | -webkit-appearance: button; /* 1 */ 312 | font: inherit; /* 2 */ 313 | } 314 | 315 | /* Interactive 316 | ========================================================================== */ 317 | 318 | /* 319 | * Add the correct display in Edge, IE 10+, and Firefox. 320 | */ 321 | 322 | details { 323 | display: block; 324 | } 325 | 326 | /* 327 | * Add the correct display in all browsers. 328 | */ 329 | 330 | summary { 331 | display: list-item; 332 | } 333 | 334 | /* Misc 335 | ========================================================================== */ 336 | 337 | /** 338 | * Add the correct display in IE 10+. 339 | */ 340 | 341 | template { 342 | display: none; 343 | } 344 | 345 | /** 346 | * Add the correct display in IE 10. 347 | */ 348 | 349 | [hidden] { 350 | display: none; 351 | } 352 | -------------------------------------------------------------------------------- /src/components/TerminalWindow.svelte: -------------------------------------------------------------------------------- 1 | 122 | 123 | 124 | 125 | 128 | 129 | 132 | 133 | 134 |
135 |
136 |
137 |
138 |
139 |
140 | 141 |
142 | 143 |
144 | background: 145 | {toFzfColorName(currentBg || '').toUpperCase() || '---'} 146 | {#if currentFg}   foreground: {toFzfColorName(currentFg || '').toUpperCase() || '---'}{/if} 149 |
150 | 151 | 153 | 154 |
155 | 156 | 157 | 158 |
159 | 160 | 253 | --------------------------------------------------------------------------------