├── examples ├── .gitkeep ├── README.md ├── yabairc ├── skhdrc └── services.nix ├── .eslintignore ├── apps ├── yakite-website │ └── .gitkeep ├── yakite-daemon │ ├── src │ │ ├── index.ts │ │ ├── common │ │ │ └── logger.ts │ │ └── bin │ │ │ └── yakite-daemon.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ └── CHANGELOG.md ├── yakite-bridge │ ├── src │ │ ├── index.ts │ │ ├── surface.ts │ │ └── window.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ └── CHANGELOG.md ├── krohnkite-core │ ├── src │ │ ├── types │ │ │ └── logger.types.ts │ │ ├── index.ts │ │ ├── bridge │ │ │ ├── surface.ts │ │ │ ├── window.ts │ │ │ └── index.ts │ │ ├── engine │ │ │ ├── layouts │ │ │ │ ├── floating.ts │ │ │ │ ├── index.ts │ │ │ │ ├── monocle.ts │ │ │ │ ├── stair.ts │ │ │ │ ├── spread.ts │ │ │ │ ├── spiral.ts │ │ │ │ ├── cascade.ts │ │ │ │ ├── tile.ts │ │ │ │ ├── quarter.ts │ │ │ │ ├── utils.ts │ │ │ │ ├── three-column.ts │ │ │ │ └── part.ts │ │ │ ├── store │ │ │ │ ├── layout.ts │ │ │ │ └── window.ts │ │ │ └── window.ts │ │ ├── config.ts │ │ └── utils │ │ │ ├── func.ts │ │ │ └── rect.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ ├── LICENSE │ └── CHANGELOG.md ├── yakite │ ├── package.json │ ├── Cargo.toml │ └── CHANGELOG.md ├── yakite-config │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── schema.json │ └── CHANGELOG.md ├── yakite-message │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ ├── src │ │ └── json-schema-message.ts │ ├── tsup.config.bundled_9uwz74s0rfa.mjs │ └── CHANGELOG.md ├── yakite-yabai │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ ├── src │ │ ├── yabai.test.ts │ │ ├── yabai.types.ts │ │ ├── yabai.zod.ts │ │ └── index.ts │ ├── tsup.config.bundled_ix2z3z4pb2s.mjs │ └── CHANGELOG.md └── yakite-toast │ ├── package.json │ ├── CHANGELOG.md │ └── src │ └── yakite-toast.m ├── pnpm-workspace.yaml ├── .editorconfig ├── prettier.config.cjs ├── .vscode ├── tasks.json ├── launch.json └── settings.json ├── .eslintrc.cjs ├── .envrc ├── .changeset ├── README.md └── config.json ├── tsconfig.json ├── scripts ├── fuck-github-action.ts └── changesets-extra.ts ├── LICENSE ├── .github ├── workflows │ ├── release-crates.yml │ ├── release-bin.yml │ ├── version.yml │ └── release.yml └── GIT_COMMIT_SPECIFIC.md ├── package.json ├── .gitignore ├── README.md └── flake.nix /examples/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /apps/yakite-website/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/yakite-daemon/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common/logger' 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in direct subdirs of apps/ 3 | - 'apps/*' 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | charset = utf-8 7 | 8 | -------------------------------------------------------------------------------- /apps/yakite-bridge/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bridge' 2 | export * from './surface' 3 | export * from './window' 4 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | // prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs 2 | 3 | /** @type {import("prettier").Config} */ 4 | module.exports = { 5 | printWidth: 60, 6 | } 7 | 8 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/types/logger.types.ts: -------------------------------------------------------------------------------- 1 | export interface Logger { 2 | info: (...args: any) => any 3 | warn: (...args: any) => any 4 | error: (...args: any) => any 5 | debug: (...args: any) => any 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "type": "shell", 7 | "command": "pnpm -r build", 8 | "problemMatcher": [] 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /apps/yakite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yakite", 3 | "private": true, 4 | "version": "0.1.10", 5 | "changesetsExtra": { 6 | "language": "rust", 7 | "sources": [], 8 | "versionUpdatePolicy": "auto" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | services.nix is about the configuration of [nix-darwin]. If you do not use it, please refer to this file and put the corresponding configuration into your own skhdrc and yabai configuration files. 2 | 3 | [nix-darwin]: https://github.com/LnL7/nix-darwin/ 4 | -------------------------------------------------------------------------------- /examples/yabairc: -------------------------------------------------------------------------------- 1 | yabai -m config focus_follows_mouse off 2 | yabai -m config mouse_follows_focus off 3 | yabai -m config window_opacity 0.900000 4 | 5 | borders active_color=0xffe1e3e4 inactive_color=0xff494d64 width=5.0 2>/dev/null 1>&2 & 6 | 7 | yakite-daemon 2>/dev/null 1>&2 & 8 | -------------------------------------------------------------------------------- /apps/krohnkite-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | // paths 5 | "outDir": "./dist", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": [ 9 | "./src/*" 10 | ] 11 | }, 12 | }, 13 | "include": [ 14 | "./src", 15 | "./tsup.config.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/yakite-bridge/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | // paths 5 | "outDir": "./dist", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": [ 9 | "./src/*" 10 | ] 11 | }, 12 | }, 13 | "include": [ 14 | "./src", 15 | "./tsup.config.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/yakite-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | // paths 5 | "outDir": "./dist", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": [ 9 | "./src/*" 10 | ] 11 | }, 12 | }, 13 | "include": [ 14 | "./src", 15 | "./tsup.config.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/yakite-message/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | // paths 5 | "outDir": "./dist", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": [ 9 | "./src/*" 10 | ] 11 | }, 12 | }, 13 | "include": [ 14 | "./src", 15 | "./tsup.config.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/yakite-yabai/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | // paths 5 | "outDir": "./dist", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": [ 9 | "./src/*" 10 | ] 11 | }, 12 | }, 13 | "include": [ 14 | "./src", 15 | "./tsup.config.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/yakite-daemon/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | // paths 5 | "outDir": "./dist", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": [ 9 | "./src/*" 10 | ] 11 | }, 12 | }, 13 | "include": [ 14 | "./src", 15 | "./tsup.config.ts", 16 | "./src/config/schema.json", 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /apps/yakite-bridge/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | import { $ } from 'execa' 3 | 4 | export default defineConfig({ 5 | entry: ['src/index.ts'], 6 | outDir: 'dist', 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | format: 'esm', 11 | onSuccess: async () => { 12 | await $`tsc --emitDeclarationOnly --declaration` 13 | await $`tsc-alias` 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /apps/yakite-config/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | import { $ } from 'execa' 3 | 4 | export default defineConfig({ 5 | entry: ['src/index.ts'], 6 | outDir: 'dist', 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | format: 'esm', 11 | onSuccess: async () => { 12 | await $`tsc --emitDeclarationOnly --declaration` 13 | await $`tsc-alias` 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /apps/yakite-yabai/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | import { $ } from 'execa' 3 | 4 | export default defineConfig({ 5 | entry: ['src/index.ts'], 6 | outDir: 'dist', 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | format: 'esm', 11 | onSuccess: async () => { 12 | await $`tsc --emitDeclarationOnly --declaration` 13 | await $`tsc-alias` 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /apps/krohnkite-core/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | import { $ } from 'execa' 3 | 4 | export default defineConfig({ 5 | entry: ['src/index.ts'], 6 | outDir: 'dist', 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | format: 'esm', 11 | onSuccess: async () => { 12 | await $`tsc --emitDeclarationOnly --declaration` 13 | await $`tsc-alias` 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /apps/yakite-message/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | import { $ } from 'execa' 3 | 4 | export default defineConfig({ 5 | entry: ['src/index.ts'], 6 | outDir: 'dist', 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | format: 'esm', 11 | onSuccess: async () => { 12 | await $`tsc --emitDeclarationOnly --declaration` 13 | await $`tsc-alias` 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /apps/krohnkite-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "krohnkite-core", 3 | "version": "0.1.10", 4 | "description": "", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/src/index.d.ts", 8 | "scripts": { 9 | "build": "tsup", 10 | "type:check": "tsc --noemit", 11 | "watch": "tsup --watch" 12 | }, 13 | "keywords": [], 14 | "author": "esjeon", 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /apps/yakite-daemon/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | import { $ } from 'execa' 3 | 4 | export default defineConfig({ 5 | entry: ['src/bin/yakite-daemon.ts'], 6 | outDir: 'dist/bin', 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | format: 'esm', 11 | onSuccess: async () => { 12 | await $`tsc --emitDeclarationOnly --declaration` 13 | await $`tsc-alias` 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /apps/yakite-toast/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yakite-toast", 3 | "private": true, 4 | "description": "A cli program that displays a toast window containing the current layout information when the yakite layout changes", 5 | "version": "0.1.10", 6 | "changesetsExtra": { 7 | "language": "objective-c", 8 | "sources": ["src/yakite-toast.m"], 9 | "versionUpdatePolicy": "source-code-replacement", 10 | "prevVersion": "0.1.10" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | overrides: [ 4 | { 5 | files: ['*.js', '*.jsx', '*.ts', '*.tsx'], 6 | extends: 'standard-with-typescript', 7 | rules: { 8 | '@typescript-eslint/strict-boolean-expressions': 'off', 9 | 'array-callback-return': 'off', 10 | '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_', 'varsIgnorePattern': '^_' }] 11 | }, 12 | } 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | # shellcheck shell=sh 2 | if ! has nix_direnv_version || ! nix_direnv_version 2.4.0; then 3 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.4.0/direnvrc" "sha256-XQzUAvL6pysIJnRJyR7uVpmUSZfc7LSgWQwq/4mBr1U=" 4 | fi 5 | 6 | nix_direnv_watch_file flake.nix 7 | 8 | if ! use flake . --accept-flake-config --impure; then 9 | echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2 10 | fi 11 | -------------------------------------------------------------------------------- /apps/yakite-message/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yakite-message", 3 | "version": "0.1.10", 4 | "description": "A dynamic tiled window management that bridges the gap between yabai and krohnkite", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/src/index.d.ts", 8 | "scripts": { 9 | "build": "tsup", 10 | "type:check": "tsc --noemit", 11 | "watch": "tsup --watch" 12 | }, 13 | "author": "I-Want-ToBelieve", 14 | "license": "MIT", 15 | "dependencies": {} 16 | } 17 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './manager' 2 | export * from './config' 3 | export * from './action' 4 | export * from './utils/func' 5 | export * from './utils/rect' 6 | export type * from './types/logger.types' 7 | export * from './bridge' 8 | export * from './bridge/surface' 9 | export * from './bridge/window' 10 | export * from './engine' 11 | export * from './engine/window' 12 | export * from './engine/layouts' 13 | export * from './engine/store/window' 14 | export * from './engine/store/layout' 15 | -------------------------------------------------------------------------------- /apps/yakite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "yakite" 3 | version = "0.1.10" 4 | edition = "2021" 5 | description = "A dynamic tiled window management that bridges the gap between yabai and krohnkite" 6 | license = "MIT" 7 | 8 | [dependencies] 9 | zmq = "0.10.0" 10 | log = "0.4" 11 | fern = "0.6" 12 | humantime = "2.1.0" 13 | serde_json = "1.0" 14 | 15 | [dependencies.clap] 16 | version = "4.4.7" 17 | features = [ "derive" ] 18 | 19 | [dependencies.serde] 20 | version = "1.0" 21 | features = [ "derive" ] 22 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /apps/yakite-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yakite-config", 3 | "version": "0.1.10", 4 | "description": "A dynamic tiled window management that bridges the gap between yabai and krohnkite", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/src/index.d.ts", 8 | "scripts": { 9 | "build": "tsup", 10 | "type:check": "tsc --noemit", 11 | "watch": "tsup --watch" 12 | }, 13 | "author": "I-Want-ToBelieve", 14 | "license": "MIT", 15 | "dependencies": { 16 | "conf": "^12.0.0", 17 | "krohnkite-core": "workspace:*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "start", 11 | "skipFiles": ["/**"], 12 | "preLaunchTask": "build", 13 | "program": "${workspaceFolder}/apps/yakite-daemon/dist/bin/yakite-daemon.js", 14 | "outFiles": [ 15 | "${workspaceFolder}/apps/yakite-daemon/dist/**/*.js" 16 | ] 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /apps/yakite-yabai/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yakite-yabai", 3 | "version": "0.1.10", 4 | "description": "A dynamic tiled window management that bridges the gap between yabai and krohnkite", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/src/index.d.ts", 8 | "scripts": { 9 | "test": "bun test", 10 | "build": "tsup", 11 | "type:check": "tsc --noemit", 12 | "watch": "tsup --watch" 13 | }, 14 | "author": "I-Want-ToBelieve", 15 | "license": "MIT", 16 | "dependencies": { 17 | "execa": "^8.0.1", 18 | "parse-json": "^8.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "I-Want-ToBelieve/yakite" } 6 | ], 7 | "commit": false, 8 | "access": "public", 9 | "baseBranch": "main", 10 | "updateInternalDependencies": "patch", 11 | "privatePackages": { "version": true, "tag": true }, 12 | "ignore": [], 13 | "snapshot": { 14 | "useCalculatedVersion": true, 15 | "prereleaseTemplate": "{tag}-{commit}-{datetime}" 16 | }, 17 | "linked": [], 18 | "fixed": [["yakite-*", "krohnkite-core"]] 19 | } 20 | -------------------------------------------------------------------------------- /apps/yakite-yabai/src/yabai.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, describe } from 'bun:test' 2 | import Yabai from '.' 3 | import { spacesSchema, displaysSchema, windowsSchema } from './yabai.zod' 4 | 5 | const yabai = await Yabai.create() 6 | 7 | describe('query', () => { 8 | test('displays', () => { 9 | expect(displaysSchema.safeParse(yabai.displays).success).toBe(true) 10 | }) 11 | 12 | test('spaces', () => { 13 | expect(spacesSchema.safeParse(yabai.spaces).success).toBe(true) 14 | }) 15 | 16 | test('windows', () => { 17 | expect(windowsSchema.safeParse(yabai.windows).success).toBe(true) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /apps/yakite-daemon/src/common/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format } from 'winston' 2 | import DailyRotateFile from 'winston-daily-rotate-file' 3 | 4 | const logLevels = { 5 | fatal: 0, 6 | error: 1, 7 | warn: 2, 8 | info: 3, 9 | debug: 4, 10 | trace: 5 11 | } 12 | 13 | const logger = createLogger({ 14 | format: format.combine(format.timestamp(), format.json()), 15 | levels: logLevels, 16 | transports: [new DailyRotateFile({ 17 | dirname: '/tmp', 18 | filename: 'yakite-daemon-%DATE%.log', 19 | datePattern: 'YYYY-MM-DD-HH', 20 | zippedArchive: false, 21 | maxSize: '20m', 22 | maxFiles: '14d' 23 | })] 24 | }) 25 | 26 | export default logger 27 | -------------------------------------------------------------------------------- /apps/yakite-bridge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yakite-bridge", 3 | "version": "0.1.10", 4 | "description": "A dynamic tiled window management that bridges the gap between yabai and krohnkite", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/src/index.d.ts", 8 | "scripts": { 9 | "build": "tsup", 10 | "type:check": "tsc --noemit", 11 | "watch": "tsup --watch" 12 | }, 13 | "author": "I-Want-ToBelieve", 14 | "license": "MIT", 15 | "dependencies": { 16 | "debounce": "^2.0.0", 17 | "execa": "^8.0.1", 18 | "yakite-yabai": "workspace:*", 19 | "krohnkite-core": "workspace:*", 20 | "yakite-message": "workspace:*", 21 | "yakite-config": "workspace:*" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | // add Bun type definitions 5 | "types": ["bun-types"], 6 | // enable latest features 7 | "lib": ["ESNext"], 8 | "module": "esnext", 9 | "target": "esnext", 10 | // if TS 5.x+ 11 | "moduleResolution": "bundler", 12 | "sourceMap": true, 13 | "noEmit": false, 14 | "allowImportingTsExtensions": false, 15 | "moduleDetection": "force", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | // if TS 4.x or earlier 19 | // "moduleResolution": "nodenext", 20 | // best practices 21 | "strict": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "skipLibCheck": true, 24 | "composite": true, 25 | "downlevelIteration": true, 26 | "allowSyntheticDefaultImports": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/bridge/surface.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type Rect } from '@/utils/rect' 7 | 8 | /** 9 | * Surface provided by KWin. Surface is essentially a screen space, but 10 | * it can represent a surface, that is not currently displayed, e.g. a 11 | * virtual desktop. 12 | */ 13 | export interface IBridgeSurface { 14 | /** 15 | * Surface unique id 16 | */ 17 | readonly id: string | number 18 | 19 | /** 20 | * Should the surface be completely ignored by the script. 21 | */ 22 | readonly ignore: boolean 23 | 24 | /** 25 | * The area in which windows are placed. 26 | */ 27 | readonly workingArea: Readonly 28 | 29 | /** 30 | * The next surface. The next surface is a virtual desktop, that comes after current one. 31 | */ 32 | next: () => IBridgeSurface | null 33 | } 34 | -------------------------------------------------------------------------------- /scripts/fuck-github-action.ts: -------------------------------------------------------------------------------- 1 | import { getPackages } from '@manypkg/get-packages' 2 | import { $ } from 'execa' 3 | 4 | export type ILanguage = 'objective-c' | 'rust' 5 | export interface IPackageJson { 6 | changesetsExtra: { 7 | language: ILanguage 8 | sources: string[] 9 | versionUpdatePolicy: 'source-code-replacement' | 'auto' 10 | prevVersion?: string 11 | } 12 | } 13 | 14 | const { packages } = await getPackages(process.cwd()) 15 | 16 | packages 17 | .filter(it => 18 | ['yakite', 'yakite-toast'].includes( 19 | ( 20 | it.packageJson as typeof it.packageJson & 21 | IPackageJson 22 | ).name 23 | ) 24 | ) 25 | .forEach(it => { 26 | const { version, name } = 27 | it.packageJson as typeof it.packageJson & IPackageJson 28 | 29 | ;(async () => { 30 | await $`git push origin --delete ${name}@${version}` 31 | await $`git push origin ${name}@${version}` 32 | })().catch(e => { 33 | console.error(e) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 I-Want-ToBelieve 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 | -------------------------------------------------------------------------------- /apps/krohnkite-core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Eon S. Jeon 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 | -------------------------------------------------------------------------------- /apps/yakite-daemon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yakite-daemon", 3 | "version": "0.1.10", 4 | "description": "A dynamic tiled window management that bridges the gap between yabai and krohnkite", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "types": "./dist/src/index.d.ts", 8 | "bin": { 9 | "yakite-daemon": "./dist/bin/yakite-daemon.js" 10 | }, 11 | "scripts": { 12 | "test": "bun test", 13 | "build": "tsup", 14 | "type:check": "tsc --noemit", 15 | "watch": "tsup --watch", 16 | "to-json": "ts-json-schema-generator --tsconfig './tsconfig.json' --path './src/config/index.ts' --type 'Config' --out './src/config/schema.json'" 17 | }, 18 | "author": "I-Want-ToBelieve", 19 | "license": "MIT", 20 | "dependencies": { 21 | "debounce": "^2.0.0", 22 | "execa": "^8.0.1", 23 | "fast-json-stringify": "^5.9.1", 24 | "krohnkite-core": "workspace:*", 25 | "parse-json": "^8.0.0", 26 | "winston": "^3.11.0", 27 | "winston-daily-rotate-file": "^4.7.1", 28 | "yakite-bridge": "workspace:*", 29 | "yakite-config": "workspace:*", 30 | "yakite-message": "workspace:*", 31 | "yakite-yabai": "workspace:*", 32 | "zeromq": "6.0.0-beta.19" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/floating.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type WindowsLayout } from '.' 7 | 8 | import { WindowState, type IEngineWindow } from '../window' 9 | 10 | import { type Rect } from '../../utils/rect' 11 | import { type IWindowsManager } from '../../manager' 12 | 13 | export default class FloatingLayout implements WindowsLayout { 14 | public static readonly id = 'FloatingLayout' 15 | public static instance = new FloatingLayout() 16 | public readonly classID = FloatingLayout.id 17 | public readonly name = 'Floating Layout' 18 | public readonly icon = 'bismuth-floating' 19 | 20 | public apply ( 21 | _controller: IWindowsManager, 22 | tileables: IEngineWindow[], 23 | _area: Rect 24 | ): void { 25 | tileables.forEach( 26 | (tileable: IEngineWindow) => (tileable.state = WindowState.TiledAfloat) 27 | ) 28 | } 29 | 30 | public clone (): this { 31 | /* fake clone */ 32 | return this 33 | } 34 | 35 | public toString (): string { 36 | return 'FloatingLayout()' 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/release-crates.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Crates.io 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'yakite@*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Branch 13 | uses: actions/checkout@v4 14 | 15 | - name: Install Nix 16 | uses: cachix/install-nix-action@v23 17 | with: 18 | nix_path: nixpkgs=channel:nixos-unstable 19 | extra_nix_config: | 20 | experimental-features = nix-command flakes 21 | 22 | - name: Verify Nix Installation 23 | run: nix shell nixpkgs#nix-info -c nix-info -m 24 | 25 | - name: Install Direnv With Nix 26 | uses: aldoborrero/direnv-nix-action@v2 27 | with: 28 | use_nix_profile: true 29 | nix_channel: nixpkgs 30 | 31 | - name: Load PATH Changes 32 | run: direnv exec . sh -c 'echo $PATH' > "$GITHUB_PATH" 33 | - name: Load other environment changes 34 | run: direnv export gha >> "$GITHUB_ENV" 35 | 36 | - name: Build 37 | run: cd $DEVENV_ROOT/apps/yakite; cargo build --release 38 | 39 | 40 | - name: Publish To Crates.io 41 | run: cd $DEVENV_ROOT/apps/yakite; cargo publish --token ${{ secrets.CRATES_IO_TOKEN }} 42 | 43 | -------------------------------------------------------------------------------- /apps/yakite-config/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_CONFIG, type Config } from 'krohnkite-core' 2 | import Conf from 'conf' 3 | 4 | export interface YakiteConfig extends Config { 5 | yakite: { 6 | toast: { 7 | enable: boolean 8 | duration: number 9 | /* Top left: x = 0, y = 0 10 | Top center: x = 0.5, y = 0 11 | Top right: x = 1, y = 0 12 | Middle left: x = 0, y = 0.5 13 | Center: x = 0.5, y = 0.5 14 | Middle right: x = 1, y = 0.5 15 | Bottom left: x = 0, y = 1 16 | Bottom center: x = 0.5, y = 1 17 | Bottom right: x = 1, y = 1 */ 18 | coordinates: { 19 | x: number 20 | y: number 21 | } 22 | } 23 | autoforce: boolean 24 | } 25 | } 26 | 27 | export const YAKITE_DEFAULT_CONFIG = { 28 | ...DEFAULT_CONFIG, 29 | yakite: { 30 | toast: { 31 | enable: true, 32 | duration: 1.5, 33 | coordinates: { 34 | x: 0.5, 35 | y: 0.5 36 | } 37 | }, 38 | autoforce: true 39 | } 40 | } 41 | 42 | const conf = new Conf({ 43 | projectName: 'yakite', 44 | projectVersion: '1.0.0', 45 | configName: 'yakite', 46 | projectSuffix: '', 47 | cwd: `${process.env.HOME}/.config/yakite`, 48 | defaults: YAKITE_DEFAULT_CONFIG 49 | }) 50 | 51 | const config = conf.store 52 | 53 | export default config 54 | -------------------------------------------------------------------------------- /.github/workflows/release-bin.yml: -------------------------------------------------------------------------------- 1 | name: ReleaseBin 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'yakite-toast*' 7 | 8 | jobs: 9 | upload: 10 | runs-on: macos-latest 11 | steps: 12 | - name: Checkout Branch 13 | uses: actions/checkout@v4 14 | 15 | - name: Install Nix 16 | uses: cachix/install-nix-action@v23 17 | with: 18 | nix_path: nixpkgs=channel:nixos-unstable 19 | extra_nix_config: | 20 | experimental-features = nix-command flakes 21 | 22 | - name: Verify Nix Installation 23 | run: nix shell nixpkgs#nix-info -c nix-info -m 24 | 25 | - name: Install Direnv With Nix 26 | uses: aldoborrero/direnv-nix-action@v2 27 | with: 28 | use_nix_profile: true 29 | nix_channel: nixpkgs 30 | 31 | - name: Load PATH Changes 32 | run: direnv exec . sh -c 'echo $PATH' > "$GITHUB_PATH" 33 | - name: Load other environment changes 34 | run: direnv export gha >> "$GITHUB_ENV" 35 | 36 | - name: Build 37 | run: yakite-toast:build 38 | 39 | 40 | - name: Upload Binaries To Release 41 | uses: svenstaro/upload-release-action@v2 42 | with: 43 | repo_token: ${{ secrets.GITHUB_TOKEN }} 44 | file: ./apps/yakite-toast/dist/yakite-toast 45 | asset_name: yakite-toast 46 | tag: ${{ github.ref }} 47 | -------------------------------------------------------------------------------- /apps/yakite-message/src/json-schema-message.ts: -------------------------------------------------------------------------------- 1 | /* 2 | hello new bing please perfect this 3 | json schema { 4 | YABAI_DISPLAY_ID: { 5 | nullable: true, 6 | type: 'string' 7 | }, 8 | YABAI_WINDOW_ID: { nullable: true, type: 'string' } 9 | } 10 | 11 | Please use nullable: true, to represent null 12 | */ 13 | export const jsonSchemaMessage = { 14 | title: 'Message Schema', 15 | type: 'object', 16 | properties: { 17 | type: { 18 | type: 'string' 19 | }, 20 | message: { 21 | type: 'string' 22 | }, 23 | env: { 24 | nullable: true, 25 | type: 'object', 26 | properties: { 27 | YABAI_DISPLAY_ID: { 28 | type: 'string', 29 | nullable: true 30 | }, 31 | YABAI_WINDOW_ID: { 32 | type: 'string', 33 | nullable: true 34 | }, 35 | YABAI_PROCESS_ID: { 36 | type: 'string', 37 | nullable: true 38 | }, 39 | YABAI_RECENT_PROCESS_ID: { 40 | type: 'string', 41 | nullable: true 42 | }, 43 | YABAI_SPACE_ID: { 44 | type: 'string', 45 | nullable: true 46 | }, 47 | YABAI_RECENT_SPACE_ID: { 48 | type: 'string', 49 | nullable: true 50 | } 51 | } 52 | }, 53 | code: { 54 | type: 'integer' 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/GIT_COMMIT_SPECIFIC.md: -------------------------------------------------------------------------------- 1 | # GIT COMMIT MESSAGE CHEAT SHEET 2 | 3 | **Proposed format of the commit message** 4 | 5 | ``` 6 | : 7 | 8 | 9 | ``` 10 | 11 | All lines are wrapped at 100 characters ! 12 | 13 | **Allowed ``** 14 | 15 | - feat (A new feature) 16 | - fix (A bug fix) 17 | - docs (Documentation only changes) 18 | - style (Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)) 19 | - perf (A code change that improves performance) 20 | - refactor (A code change that neither fixes a bug nor adds a feature) 21 | - test (Adding missing tests or correcting existing tests) 22 | - build (Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)) 23 | - ci (Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)) 24 | - chore (Other changes that don't modify src or test files) 25 | - revert (Reverts a previous commit) 26 | - release (Relase version) 27 | 28 | 29 | **Breaking changes** 30 | 31 | All breaking changes have to be mentioned in message body, on separated line: 32 | 33 | ​ _Breaks removed $browser.setUrl() method (use $browser.url(newUrl))_ 34 | ​ _Breaks ng: repeat option is no longer supported on selects (use ng:options)_ 35 | 36 | **Message body** 37 | 38 | - uses the imperative, present tense: “change” not “changed” nor “changes” 39 | - includes motivation for the change and contrasts with previous behavior 40 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type IEngineWindow } from '../window' 7 | import { type IEngine } from '..' 8 | 9 | import { type IWindowsManager } from '../../manager' 10 | import { type Action } from '../../action' 11 | 12 | import { type Rect, type RectDelta } from '../../utils/rect' 13 | 14 | export abstract class WindowsLayout { 15 | /* read-only */ 16 | 17 | static readonly id: string 18 | 19 | /** 20 | * Human-readable name of the layout. 21 | */ 22 | abstract readonly name: string 23 | 24 | /** 25 | * The icon name of the layout. 26 | */ 27 | abstract readonly icon: string 28 | 29 | /** 30 | * A string that can be used to show layout specific properties in the pop-up, 31 | * e.g. the number of master windows. 32 | */ 33 | readonly hint?: string 34 | 35 | /** 36 | * The maximum number of windows, that the layout can contain. 37 | */ 38 | readonly capacity?: number 39 | 40 | adjust? ( 41 | area: Rect, 42 | tiles: IEngineWindow[], 43 | basis: IEngineWindow, 44 | delta: RectDelta 45 | ): void 46 | 47 | abstract apply ( 48 | controller: IWindowsManager, 49 | tileables: IEngineWindow[], 50 | area: Rect 51 | ): void 52 | 53 | executeAction? (engine: IEngine, action: Action): void 54 | 55 | abstract toString (): string 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Version 2 | 3 | on: 4 | push: 5 | branches: 6 | - release/** 7 | 8 | jobs: 9 | version: 10 | name: Version 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout Branch 15 | uses: actions/checkout@v4 16 | 17 | - name: Install Nix 18 | uses: cachix/install-nix-action@v23 19 | with: 20 | nix_path: nixpkgs=channel:nixos-unstable 21 | extra_nix_config: | 22 | experimental-features = nix-command flakes 23 | 24 | - name: Verify Nix Installation 25 | run: nix shell nixpkgs#nix-info -c nix-info -m 26 | 27 | - name: Install Direnv With Nix 28 | uses: aldoborrero/direnv-nix-action@v2 29 | with: 30 | use_nix_profile: true 31 | nix_channel: nixpkgs 32 | 33 | - name: Load PATH Changes 34 | run: direnv exec . sh -c 'echo $PATH' > "$GITHUB_PATH" 35 | - name: Load other environment changes 36 | run: direnv export gha >> "$GITHUB_ENV" 37 | 38 | 39 | - name: Install Dependencies 40 | run: pnpm install 41 | 42 | - name: Setup Git User 43 | run: git-user:setup 44 | 45 | - name: Create Release Pull Request 46 | uses: changesets/action@v1 47 | with: 48 | version: pnpm run version 49 | commit: 'chore: update versions' 50 | title: 'chore: update versions' 51 | setupGitUser: false 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | 55 | -------------------------------------------------------------------------------- /apps/yakite-yabai/src/yabai.types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://transform.tools/json-to-typescript89 3 | * @see https://github.com/koekeishiya/yabai/blob/master/doc/yabai.asciidoc#command-3 4 | */ 5 | 6 | export interface YabaiFrame { 7 | x: number 8 | y: number 9 | w: number 10 | h: number 11 | } 12 | 13 | export interface YabaiDisplay { 14 | id: number 15 | uuid: string 16 | index: number 17 | frame: YabaiFrame 18 | spaces: number[] 19 | } 20 | 21 | export interface YabaiSpace { 22 | id: number 23 | uuid: string 24 | index: number 25 | label: string 26 | type: string 27 | display: number 28 | windows: number[] 29 | 'first-window': number 30 | 'last-window': number 31 | 'has-focus': boolean 32 | 'is-visible': boolean 33 | 'is-native-fullscreen': boolean 34 | } 35 | 36 | export interface YabaiWindow { 37 | id: number 38 | pid: number 39 | app: string 40 | title: string 41 | frame: YabaiFrame 42 | role: string 43 | subrole: string 44 | display: number 45 | space: number 46 | level: number 47 | layer: string 48 | opacity: number 49 | 'split-type': string 50 | 'split-child': string 51 | 'stack-index': number 52 | 'can-move': boolean 53 | 'can-resize': boolean 54 | 'has-focus': boolean 55 | 'has-shadow': boolean 56 | 'has-parent-zoom': boolean 57 | 'has-fullscreen-zoom': boolean 58 | 'is-native-fullscreen': boolean 59 | 'is-visible': boolean 60 | 'is-minimized': boolean 61 | 'is-hidden': boolean 62 | 'is-floating': boolean 63 | 'is-sticky': boolean 64 | 'is-grabbed': boolean 65 | } 66 | 67 | export type YabaiDisplays = YabaiDisplay[] 68 | export type YabaiSpaces = YabaiSpace[] 69 | export type YabaiWindows = YabaiWindow[] 70 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll": true 4 | }, 5 | "eslint.options": { 6 | // "overrideConfigFile": "eslint.config.js" 7 | }, 8 | "rust-analyzer.linkedProjects": [ 9 | "apps/yakite/Cargo.toml" 10 | ], 11 | "nixEnvSelector.nixFile": "${workspaceRoot}/flake.nix", 12 | "[rust]": { 13 | "editor.defaultFormatter": "rust-lang.rust-analyzer", 14 | "editor.tabSize": 2 15 | }, 16 | "rust-analyzer.rustfmt.extraArgs": [ 17 | "--config", 18 | "tab_spaces=2" 19 | ], 20 | "typescript.preferences.importModuleSpecifier": "non-relative", 21 | "javascript.preferences.importModuleSpecifier": "non-relative", 22 | "[objective-c]": { 23 | "editor.defaultFormatter": "xaver.clang-format" 24 | }, 25 | "[typescript]": { 26 | "editor.defaultFormatter": "numso.prettier-standard-vscode", 27 | "editor.formatOnSave": false 28 | }, 29 | "[javascript]": { 30 | "editor.defaultFormatter": "numso.prettier-standard-vscode", 31 | "editor.formatOnSave": false 32 | }, 33 | "[jsonc]": { 34 | "editor.defaultFormatter": "numso.prettier-standard-vscode", 35 | "editor.formatOnSave": true 36 | }, 37 | "[json]": { 38 | "editor.defaultFormatter": "numso.prettier-standard-vscode" 39 | }, 40 | "[md]": { 41 | "editor.defaultFormatter": "numso.prettier-standard-vscode", 42 | "editor.formatOnSave": true 43 | } 44 | // "runOnSave.statusMessageTimeout": 3000, 45 | // "runOnSave.commands": [ 46 | // { 47 | // // Match less files except names start with `_`. 48 | // "globMatch": "**/[^_]*.*", 49 | // "command": "treefmt ${file}", 50 | // "runIn": "backend", 51 | // "runningStatusMessage": "Formating ${fileBasename}", 52 | // "finishStatusMessage": "${fileBasename} formated" 53 | // } 54 | // ] 55 | } 56 | -------------------------------------------------------------------------------- /examples/skhdrc: -------------------------------------------------------------------------------- 1 | ctrl - return : kitty --single-instance -d ~ 2 | 3 | cmd - backspace : skhd -k "ctrl - space" 4 | 5 | ctrl - w : skhd -k "ctrl - up" 6 | 7 | ctrl - 1 : yabai -m space --focus 1 8 | ctrl - 2 : yabai -m space --focus 2 9 | ctrl - 3 : yabai -m space --focus 3 10 | ctrl - 4 : yabai -m space --focus 4 11 | ctrl - 5 : yabai -m space --focus 5 12 | ctrl - 6 : yabai -m space --focus 6 13 | ctrl - 7 : yabai -m space --focus 7 14 | ctrl - 8 : yabai -m space --focus 8 15 | ctrl - 9 : yabai -m space --focus 9 16 | ctrl - 0 : yabai -m space --focus 10 17 | 18 | ctrl + shift - j : yabai -m window --space prev 19 | ctrl + shift - l : yabai -m window --space next 20 | ctrl + shift - 1 : yabai -m window --space 1 21 | ctrl + shift - 2 : yabai -m window --space 2 22 | ctrl + shift - 3 : yabai -m window --space 3 23 | ctrl + shift - 4 : yabai -m window --space 4 24 | ctrl + shift - 5 : yabai -m window --space 5 25 | ctrl + shift - 6 : yabai -m window --space 6 26 | ctrl + shift - 7 : yabai -m window --space 7 27 | ctrl + shift - 8 : yabai -m window --space 8 28 | ctrl + shift - 9 : yabai -m window --space 9 29 | ctrl + shift - 0 : yabai -m window --space 10 30 | 31 | 32 | ctrl - q : yabai -m window --close 33 | ctrl - x : yabai -m window --minimize 34 | 35 | ctrl - t : yakite action toggle-tile-layout 36 | ctrl - m : yakite action toggle-monocle-layout 37 | ctrl - f : yakite action toggle-active-window-floating 38 | ctrl - k : yakite action focus-next-window 39 | ctrl - i : yakite action focus-previous-window 40 | ctrl + shift - k : yakite action move-active-window-to-next-position 41 | ctrl + shift - i : yakite action move-active-window-to-previous-position 42 | ctrl + shift - m : yakite action push-active-window-into-master-area-front 43 | 44 | ctrl - 0x2A : yakite action switch-to-next-layout 45 | 46 | ctrl - j : yakite action decrease-layout-master-area-size 47 | ctrl - l : yakite action increase-layout-master-area-size 48 | -------------------------------------------------------------------------------- /apps/yakite-daemon/src/bin/yakite-daemon.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import zmq from 'zeromq' 3 | 4 | import Yabai from 'yakite-yabai' 5 | import logger from '@/common/logger' 6 | import config from 'yakite-config' 7 | import { $ } from 'execa' 8 | import { WindowsManager } from 'krohnkite-core' 9 | import parseJson from 'parse-json' 10 | import fastJson from 'fast-json-stringify' 11 | import { type Message } from 'yakite-message' 12 | import { jsonSchemaMessage } from 'yakite-message' 13 | import { YakiteBridge } from 'yakite-bridge' 14 | 15 | process.title = 'yakite-daemon' 16 | 17 | // TODO use pid file lock 18 | try { 19 | await Promise.all([$`killall yakite`, $`pkill -f yakite-daemon`]) 20 | } catch (error) { 21 | } 22 | 23 | const sock = new zmq.Reply() 24 | await sock.bind('tcp://127.0.0.1:20206') 25 | 26 | /** 27 | * real state(from yabai cli) <-> clone state (the yabai class)<-> bridge state (the YakiteBridge)<-> engine state(the WindowsManager) 28 | * 29 | */ 30 | const yabai = await Yabai.create() 31 | const bridge = await YakiteBridge.create(yabai, config, logger) 32 | 33 | const wm = new WindowsManager( 34 | bridge, 35 | config, 36 | logger 37 | ) 38 | 39 | wm.start() 40 | 41 | process.on('exit', () => { 42 | void yabai.drop?.() 43 | }) 44 | 45 | const stringify = fastJson(jsonSchemaMessage) 46 | 47 | // message queue loop 48 | for await (const [buffer] of sock) { 49 | const json = buffer.toString() 50 | 51 | const { env, message, type } = parseJson(json) as unknown as Message 52 | 53 | logger.info({ env, message, type }) 54 | 55 | switch (type) { 56 | case 'event': 57 | await bridge.wrappedListeners[message](env as any) 58 | break 59 | case 'action': 60 | bridge.actions[message]() 61 | break 62 | default: 63 | break 64 | } 65 | 66 | logger.info('sock.send()') 67 | 68 | await sock.send(stringify({ env, message, type, code: 200 })) 69 | } 70 | -------------------------------------------------------------------------------- /apps/yakite-yabai/src/yabai.zod.ts: -------------------------------------------------------------------------------- 1 | // Generated by ts-to-zod 2 | import { z } from 'zod' 3 | 4 | export const frameSchema = z.object({ 5 | x: z.number(), 6 | y: z.number(), 7 | w: z.number(), 8 | h: z.number() 9 | }) 10 | 11 | export const displaySchema = z.object({ 12 | id: z.number(), 13 | uuid: z.string(), 14 | index: z.number(), 15 | frame: frameSchema, 16 | spaces: z.array(z.number()) 17 | }) 18 | 19 | export const spaceSchema = z.object({ 20 | id: z.number(), 21 | uuid: z.string(), 22 | index: z.number(), 23 | label: z.string(), 24 | type: z.string(), 25 | display: z.number(), 26 | windows: z.array(z.number()), 27 | 'first-window': z.number(), 28 | 'last-window': z.number(), 29 | 'has-focus': z.boolean(), 30 | 'is-visible': z.boolean(), 31 | 'is-native-fullscreen': z.boolean() 32 | }) 33 | 34 | export const windowSchema = z.object({ 35 | id: z.number(), 36 | pid: z.number(), 37 | app: z.string(), 38 | title: z.string(), 39 | frame: frameSchema, 40 | role: z.string(), 41 | subrole: z.string(), 42 | display: z.number(), 43 | space: z.number(), 44 | level: z.number(), 45 | layer: z.string(), 46 | opacity: z.number(), 47 | 'split-type': z.string(), 48 | 'split-child': z.string(), 49 | 'stack-index': z.number(), 50 | 'can-move': z.boolean(), 51 | 'can-resize': z.boolean(), 52 | 'has-focus': z.boolean(), 53 | 'has-shadow': z.boolean(), 54 | 'has-parent-zoom': z.boolean(), 55 | 'has-fullscreen-zoom': z.boolean(), 56 | 'is-native-fullscreen': z.boolean(), 57 | 'is-visible': z.boolean(), 58 | 'is-minimized': z.boolean(), 59 | 'is-hidden': z.boolean(), 60 | 'is-floating': z.boolean(), 61 | 'is-sticky': z.boolean(), 62 | 'is-grabbed': z.boolean() 63 | }) 64 | 65 | export const displaysSchema = z.array(displaySchema) 66 | 67 | export const spacesSchema = z.array(spaceSchema) 68 | 69 | export const windowsSchema = z.array(windowSchema) 70 | -------------------------------------------------------------------------------- /apps/yakite-yabai/tsup.config.bundled_ix2z3z4pb2s.mjs: -------------------------------------------------------------------------------- 1 | // tsup.config.ts 2 | import { defineConfig } from "tsup"; 3 | import { $ } from "execa"; 4 | var tsup_config_default = defineConfig({ 5 | entry: ["src/index.ts"], 6 | outDir: "dist", 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | format: "esm", 11 | onSuccess: async () => { 12 | await $`tsc --emitDeclarationOnly --declaration`; 13 | await $`tsc-alias`; 14 | } 15 | }); 16 | export { 17 | tsup_config_default as default 18 | }; 19 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidHN1cC5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9faW5qZWN0ZWRfZmlsZW5hbWVfXyA9IFwiL1VzZXJzL2kud2FudC50by5iZWxpZXZlL2dpdC53b3Jrc3BhY2VzL2pzLndvcmtzcGFjZXMveWFraXRlL2FwcHMveWFraXRlLXlhYmFpL3RzdXAuY29uZmlnLnRzXCI7Y29uc3QgX19pbmplY3RlZF9kaXJuYW1lX18gPSBcIi9Vc2Vycy9pLndhbnQudG8uYmVsaWV2ZS9naXQud29ya3NwYWNlcy9qcy53b3Jrc3BhY2VzL3lha2l0ZS9hcHBzL3lha2l0ZS15YWJhaVwiO2NvbnN0IF9faW5qZWN0ZWRfaW1wb3J0X21ldGFfdXJsX18gPSBcImZpbGU6Ly8vVXNlcnMvaS53YW50LnRvLmJlbGlldmUvZ2l0LndvcmtzcGFjZXMvanMud29ya3NwYWNlcy95YWtpdGUvYXBwcy95YWtpdGUteWFiYWkvdHN1cC5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd0c3VwJ1xuaW1wb3J0IHsgJCB9IGZyb20gJ2V4ZWNhJ1xuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBlbnRyeTogWydzcmMvaW5kZXgudHMnXSxcbiAgb3V0RGlyOiAnZGlzdCcsXG4gIHNwbGl0dGluZzogZmFsc2UsXG4gIHNvdXJjZW1hcDogdHJ1ZSxcbiAgY2xlYW46IHRydWUsXG4gIGZvcm1hdDogJ2VzbScsXG4gIG9uU3VjY2VzczogYXN5bmMgKCkgPT4ge1xuICAgIGF3YWl0ICRgdHNjIC0tZW1pdERlY2xhcmF0aW9uT25seSAtLWRlY2xhcmF0aW9uYFxuICAgIGF3YWl0ICRgdHNjLWFsaWFzYFxuICB9XG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF3WCxTQUFTLG9CQUFvQjtBQUNyWixTQUFTLFNBQVM7QUFFbEIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsT0FBTyxDQUFDLGNBQWM7QUFBQSxFQUN0QixRQUFRO0FBQUEsRUFDUixXQUFXO0FBQUEsRUFDWCxXQUFXO0FBQUEsRUFDWCxPQUFPO0FBQUEsRUFDUCxRQUFRO0FBQUEsRUFDUixXQUFXLFlBQVk7QUFDckIsVUFBTTtBQUNOLFVBQU07QUFBQSxFQUNSO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K 20 | -------------------------------------------------------------------------------- /apps/yakite-message/tsup.config.bundled_9uwz74s0rfa.mjs: -------------------------------------------------------------------------------- 1 | // tsup.config.ts 2 | import { defineConfig } from "tsup"; 3 | import { $ } from "execa"; 4 | var tsup_config_default = defineConfig({ 5 | entry: ["src/index.ts"], 6 | outDir: "dist", 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | format: "esm", 11 | onSuccess: async () => { 12 | await $`tsc --emitDeclarationOnly --declaration`; 13 | await $`tsc-alias`; 14 | } 15 | }); 16 | export { 17 | tsup_config_default as default 18 | }; 19 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidHN1cC5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9faW5qZWN0ZWRfZmlsZW5hbWVfXyA9IFwiL1VzZXJzL2kud2FudC50by5iZWxpZXZlL2dpdC53b3Jrc3BhY2VzL2pzLndvcmtzcGFjZXMveWFraXRlL2FwcHMveWFraXRlLW1lc3NhZ2UvdHN1cC5jb25maWcudHNcIjtjb25zdCBfX2luamVjdGVkX2Rpcm5hbWVfXyA9IFwiL1VzZXJzL2kud2FudC50by5iZWxpZXZlL2dpdC53b3Jrc3BhY2VzL2pzLndvcmtzcGFjZXMveWFraXRlL2FwcHMveWFraXRlLW1lc3NhZ2VcIjtjb25zdCBfX2luamVjdGVkX2ltcG9ydF9tZXRhX3VybF9fID0gXCJmaWxlOi8vL1VzZXJzL2kud2FudC50by5iZWxpZXZlL2dpdC53b3Jrc3BhY2VzL2pzLndvcmtzcGFjZXMveWFraXRlL2FwcHMveWFraXRlLW1lc3NhZ2UvdHN1cC5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd0c3VwJ1xuaW1wb3J0IHsgJCB9IGZyb20gJ2V4ZWNhJ1xuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBlbnRyeTogWydzcmMvaW5kZXgudHMnXSxcbiAgb3V0RGlyOiAnZGlzdCcsXG4gIHNwbGl0dGluZzogZmFsc2UsXG4gIHNvdXJjZW1hcDogdHJ1ZSxcbiAgY2xlYW46IHRydWUsXG4gIGZvcm1hdDogJ2VzbScsXG4gIG9uU3VjY2VzczogYXN5bmMgKCkgPT4ge1xuICAgIGF3YWl0ICRgdHNjIC0tZW1pdERlY2xhcmF0aW9uT25seSAtLWRlY2xhcmF0aW9uYFxuICAgIGF3YWl0ICRgdHNjLWFsaWFzYFxuICB9XG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUE4WCxTQUFTLG9CQUFvQjtBQUMzWixTQUFTLFNBQVM7QUFFbEIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsT0FBTyxDQUFDLGNBQWM7QUFBQSxFQUN0QixRQUFRO0FBQUEsRUFDUixXQUFXO0FBQUEsRUFDWCxXQUFXO0FBQUEsRUFDWCxPQUFPO0FBQUEsRUFDUCxRQUFRO0FBQUEsRUFDUixXQUFXLFlBQVk7QUFDckIsVUFBTTtBQUNOLFVBQU07QUFBQSxFQUNSO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | version: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout Branch 15 | uses: actions/checkout@v4 16 | with: 17 | ssh-key: ${{ secrets.DEPLOY_KEY }} 18 | 19 | - name: Install Nix 20 | uses: cachix/install-nix-action@v23 21 | with: 22 | nix_path: nixpkgs=channel:nixos-unstable 23 | extra_nix_config: | 24 | experimental-features = nix-command flakes 25 | 26 | - name: Verify Nix Installation 27 | run: nix shell nixpkgs#nix-info -c nix-info -m 28 | 29 | - name: Install Direnv With Nix 30 | uses: aldoborrero/direnv-nix-action@v2 31 | with: 32 | use_nix_profile: true 33 | nix_channel: nixpkgs 34 | 35 | - name: Load PATH Changes 36 | run: direnv exec . sh -c 'echo $PATH' > "$GITHUB_PATH" 37 | - name: Load other environment changes 38 | run: direnv export gha >> "$GITHUB_ENV" 39 | 40 | - name: Setup 41 | run: pnpm run setup 42 | 43 | - name: Setup Git User 44 | run: git-user:setup 45 | 46 | - name: Create Release Pull Request Or Publish To NPM 47 | # https://github.com/changesets/action 48 | uses: changesets/action@v1 49 | with: 50 | version: pnpm run version 51 | commit: 'chore: update versions' 52 | title: 'chore: update versions' 53 | # this expects you to have a script called release which does a build for your packages and calls changeset publish 54 | publish: pnpm run release 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # https://stackoverflow.com/questions/75348291/how-to-trigger-github-actions-workflow-whenever-a-new-tag-was-pushed 57 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 58 | 59 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/config.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | export interface Config { 7 | experimentalBackend: boolean 8 | 9 | // #region Layout 10 | layoutOrder: string[] 11 | monocleMaximize: boolean 12 | maximizeSoleTile: boolean 13 | monocleMinimizeRest: boolean // KWin-specific 14 | untileByDragging: boolean 15 | // #endregion 16 | 17 | // #region Features 18 | keepFloatAbove: boolean 19 | noTileBorder: boolean 20 | limitTileWidthRatio: number 21 | // #endregion 22 | 23 | // #region Gap 24 | gaps: { 25 | screen: { 26 | top: number 27 | left: number 28 | right: number 29 | bottom: number 30 | } 31 | tileLayout: number 32 | } 33 | // #endregion 34 | 35 | // #region Behavior 36 | newWindowAsMaster: boolean 37 | // #endregion 38 | 39 | ignore: { 40 | class: string[] 41 | title: string[] 42 | screen: number[] 43 | role: string[] 44 | } 45 | 46 | floating: { 47 | class: string[] 48 | title: string[] 49 | } 50 | // #endregion 51 | } 52 | 53 | export const DEFAULT_CONFIG: Readonly = { 54 | experimentalBackend: false, 55 | 56 | layoutOrder: [ 57 | 'TileLayout', 58 | 'MonocleLayout', 59 | 'ThreeColumnLayout', 60 | 'SpreadLayout', 61 | 'StairLayout', 62 | 'SpiralLayout', 63 | 'QuarterLayout', 64 | 'CascadeLayout' 65 | ], 66 | monocleMaximize: false, 67 | maximizeSoleTile: false, 68 | monocleMinimizeRest: false, 69 | untileByDragging: false, 70 | 71 | keepFloatAbove: false, 72 | noTileBorder: false, 73 | limitTileWidthRatio: 0, 74 | newWindowAsMaster: true, 75 | gaps: { 76 | screen: { 77 | top: 40, 78 | left: 20, 79 | right: 20, 80 | bottom: 100 81 | }, 82 | tileLayout: 20 83 | }, 84 | ignore: { 85 | class: [], 86 | title: [], 87 | screen: [], 88 | role: [] 89 | }, 90 | floating: { 91 | class: [], 92 | title: [] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/bridge/window.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type Rect } from '@/utils/rect' 7 | import { type IBridgeSurface } from './surface' 8 | 9 | /** 10 | * KWin window representation. 11 | */ 12 | export interface IBridgeWindow { 13 | /** 14 | * Is the window is currently set to be fullscreen 15 | */ 16 | readonly fullScreen: boolean 17 | 18 | /** 19 | * Window geometry: its coordinates, width and height 20 | */ 21 | readonly geometry: Readonly 22 | 23 | /** 24 | * Window unique id 25 | */ 26 | readonly id: string | number 27 | 28 | /** 29 | * Whether it window is in maximized state 30 | */ 31 | readonly maximized: boolean 32 | 33 | /** 34 | * Whether the window should be completely ignored by the script 35 | */ 36 | readonly shouldIgnore: boolean 37 | 38 | /** 39 | * Whether the window should float according to the some predefined rules 40 | */ 41 | readonly shouldFloat: boolean 42 | 43 | /** 44 | * The screen number the window is currently at 45 | */ 46 | readonly screen: number 47 | 48 | /** 49 | * Whether the window is focused right now 50 | */ 51 | readonly active: boolean 52 | 53 | /** 54 | * Whether the window is a dialog window 55 | */ 56 | readonly isDialog: boolean 57 | 58 | /** 59 | * Window's current surface 60 | */ 61 | surface: IBridgeSurface 62 | 63 | /** 64 | * Whether the window is minimized 65 | */ 66 | minimized: boolean 67 | 68 | /** 69 | * Whether the window is shaded 70 | */ 71 | shaded: boolean 72 | 73 | /** 74 | * Commit the window properties to the KWin, i.e. "show the results of our manipulations to the user" 75 | * @param geometry 76 | * @param noBorder 77 | * @param keepAbove 78 | */ 79 | commit: (geometry?: Rect, noBorder?: boolean, keepAbove?: boolean) => Promise 80 | 81 | /** 82 | * Whether the window is visible on the specified surface 83 | * @param surface the surface to check against 84 | */ 85 | visibleOn: (surface: IBridgeSurface) => boolean 86 | } 87 | -------------------------------------------------------------------------------- /apps/yakite-bridge/src/surface.ts: -------------------------------------------------------------------------------- 1 | import { type IBridgeSurface, type Config } from 'krohnkite-core' 2 | import { Rect } from 'krohnkite-core' 3 | import type Yabai from 'yakite-yabai' 4 | import { type YabaiSpace, type YabaiDisplay } from 'yakite-yabai' 5 | 6 | export class YakiteBridgeSurface implements IBridgeSurface { 7 | public readonly ignore: boolean 8 | public readonly workingArea: Rect 9 | public readonly id: string 10 | 11 | constructor ( 12 | public readonly displayId: YabaiDisplay['id'], 13 | public readonly spaceId: YabaiSpace['id'], 14 | private readonly config: Config, 15 | private readonly yabai: Yabai 16 | ) { 17 | this.id = this.generateId() 18 | 19 | this.ignore = this.config.ignore.screen.includes(this.display.index) 20 | 21 | this.workingArea = Rect.fromFrame( 22 | this.display.frame 23 | ) 24 | } 25 | 26 | private savedDisplay: YabaiDisplay | null = null 27 | private savedSpace: YabaiSpace | null = null 28 | 29 | public get display (): YabaiDisplay { 30 | const display = this.yabai.displays.find((it) => it.id === this.displayId) 31 | 32 | if (display) { 33 | this.savedDisplay = display 34 | } 35 | 36 | return this.savedDisplay as YabaiDisplay 37 | } 38 | 39 | public get space (): YabaiSpace { 40 | const space = this.yabai.spaces.find((it) => +it.id === +this.spaceId) 41 | 42 | if (space) { 43 | this.savedSpace = space 44 | } 45 | 46 | return this.savedSpace as YabaiSpace 47 | } 48 | 49 | public next (): IBridgeSurface | null { 50 | const { currentDisplay, spaces } = this.yabai 51 | const currentDisplaySpaces = spaces.filter((it) => it.display === currentDisplay.index) 52 | // This is the last virtual desktop 53 | 54 | const next = currentDisplaySpaces.find((it) => it.index === this.space.index + 1) 55 | if (!next) { 56 | return null 57 | } 58 | 59 | return new YakiteBridgeSurface( 60 | this.display.id, 61 | next.id, 62 | this.config, 63 | this.yabai 64 | ) 65 | } 66 | 67 | private generateId (): string { 68 | return `${this.displayId}-${this.spaceId}` 69 | } 70 | 71 | public toString (): string { 72 | return `YakiteBridgeSurface(${this.display.index}, ${this.space.index})` 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/monocle.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type WindowsLayout } from '.' 7 | 8 | import { WindowState, type IEngineWindow } from '../window' 9 | 10 | import { 11 | type Action, 12 | FocusBottomWindow, 13 | FocusLeftWindow, 14 | FocusNextWindow, 15 | FocusPreviousWindow, 16 | FocusRightWindow, 17 | FocusUpperWindow 18 | } from '../../action' 19 | 20 | import { type Rect } from '../../utils/rect' 21 | 22 | import { type IWindowsManager } from '../../manager' 23 | import { type IEngine } from '..' 24 | import { type Config } from '@/config' 25 | 26 | export default class MonocleLayout implements WindowsLayout { 27 | public static readonly id = 'MonocleLayout' 28 | public readonly classID = MonocleLayout.id 29 | public readonly name = 'Monocle Layout' 30 | public readonly icon = 'bismuth-monocle' 31 | 32 | private readonly config: Config 33 | 34 | constructor (config: Config) { 35 | this.config = config 36 | } 37 | 38 | public apply ( 39 | controller: IWindowsManager, 40 | tileables: IEngineWindow[], 41 | area: Rect 42 | ): void { 43 | /* Tile all tileables */ 44 | tileables.forEach((tile) => { 45 | tile.state = this.config.monocleMaximize 46 | ? WindowState.Maximized 47 | : WindowState.Tiled 48 | 49 | tile.geometry = area 50 | }) 51 | } 52 | 53 | public clone (): this { 54 | /* fake clone */ 55 | return this 56 | } 57 | 58 | public executeAction (engine: IEngine, action: Action): void { 59 | if ( 60 | action instanceof FocusUpperWindow || 61 | action instanceof FocusLeftWindow || 62 | action instanceof FocusPreviousWindow 63 | ) { 64 | engine.focusOrder(-1, this.config.monocleMinimizeRest) 65 | } else if ( 66 | action instanceof FocusBottomWindow || 67 | action instanceof FocusRightWindow || 68 | action instanceof FocusNextWindow 69 | ) { 70 | engine.focusOrder(1, this.config.monocleMinimizeRest) 71 | } else { 72 | action.executeWithoutLayoutOverride() 73 | } 74 | } 75 | 76 | public toString (): string { 77 | return 'MonocleLayout()' 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /apps/yakite/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # yakite 2 | 3 | ## 0.1.10 4 | 5 | ### Patch Changes 6 | 7 | - [`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - fix: remove the duplicates FloatingLayout 8 | 9 | ## 0.1.9 10 | 11 | ### Patch Changes 12 | 13 | - [`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore(yakite-toast): add arm support 14 | 15 | ## 0.1.8 16 | 17 | ### Patch Changes 18 | 19 | - [`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: enable file log 20 | 21 | ## 0.1.7 22 | 23 | ### Patch Changes 24 | 25 | - [`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - feat: fuck github action 26 | 27 | ## 0.1.6 28 | 29 | ### Patch Changes 30 | 31 | - [`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: use deploy key instead of a PAT. 32 | 33 | ## 0.1.5 34 | 35 | ### Patch Changes 36 | 37 | - [`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: try to trigger github action tags monitoring 38 | 39 | ## 0.1.4 40 | 41 | ### Patch Changes 42 | 43 | - [`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: hit github action more accurately 44 | 45 | ## 0.1.3 46 | 47 | ### Patch Changes 48 | 49 | - [`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: release crates and use @changesets/changelog-github 50 | 51 | ## 0.1.2 52 | 53 | ### Patch Changes 54 | 55 | - 1e7cc3b: chore: update zeromq from 6.0.0-beta.17 to 6.0.0-beta.19 56 | 57 | ## 0.1.1 58 | 59 | ### Patch Changes 60 | 61 | - 1c80812: feat: first release 62 | -------------------------------------------------------------------------------- /apps/krohnkite-core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # krohnkite-core 2 | 3 | ## 0.1.10 4 | 5 | ### Patch Changes 6 | 7 | - [`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - fix: remove the duplicates FloatingLayout 8 | 9 | ## 0.1.9 10 | 11 | ### Patch Changes 12 | 13 | - [`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore(yakite-toast): add arm support 14 | 15 | ## 0.1.8 16 | 17 | ### Patch Changes 18 | 19 | - [`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: enable file log 20 | 21 | ## 0.1.7 22 | 23 | ### Patch Changes 24 | 25 | - [`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - feat: fuck github action 26 | 27 | ## 0.1.6 28 | 29 | ### Patch Changes 30 | 31 | - [`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: use deploy key instead of a PAT. 32 | 33 | ## 0.1.5 34 | 35 | ### Patch Changes 36 | 37 | - [`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: try to trigger github action tags monitoring 38 | 39 | ## 0.1.4 40 | 41 | ### Patch Changes 42 | 43 | - [`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: hit github action more accurately 44 | 45 | ## 0.1.3 46 | 47 | ### Patch Changes 48 | 49 | - [`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: release crates and use @changesets/changelog-github 50 | 51 | ## 0.1.2 52 | 53 | ### Patch Changes 54 | 55 | - 1e7cc3b: chore: update zeromq from 6.0.0-beta.17 to 6.0.0-beta.19 56 | 57 | ## 0.1.1 58 | 59 | ### Patch Changes 60 | 61 | - 1c80812: feat: first release 62 | -------------------------------------------------------------------------------- /apps/yakite-message/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # yakite-message 2 | 3 | ## 0.1.10 4 | 5 | ### Patch Changes 6 | 7 | - [`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - fix: remove the duplicates FloatingLayout 8 | 9 | ## 0.1.9 10 | 11 | ### Patch Changes 12 | 13 | - [`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore(yakite-toast): add arm support 14 | 15 | ## 0.1.8 16 | 17 | ### Patch Changes 18 | 19 | - [`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: enable file log 20 | 21 | ## 0.1.7 22 | 23 | ### Patch Changes 24 | 25 | - [`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - feat: fuck github action 26 | 27 | ## 0.1.6 28 | 29 | ### Patch Changes 30 | 31 | - [`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: use deploy key instead of a PAT. 32 | 33 | ## 0.1.5 34 | 35 | ### Patch Changes 36 | 37 | - [`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: try to trigger github action tags monitoring 38 | 39 | ## 0.1.4 40 | 41 | ### Patch Changes 42 | 43 | - [`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: hit github action more accurately 44 | 45 | ## 0.1.3 46 | 47 | ### Patch Changes 48 | 49 | - [`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: release crates and use @changesets/changelog-github 50 | 51 | ## 0.1.2 52 | 53 | ### Patch Changes 54 | 55 | - 1e7cc3b: chore: update zeromq from 6.0.0-beta.17 to 6.0.0-beta.19 56 | 57 | ## 0.1.1 58 | 59 | ### Patch Changes 60 | 61 | - 1c80812: feat: first release 62 | -------------------------------------------------------------------------------- /apps/yakite-toast/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # yakite-toast 2 | 3 | ## 0.1.10 4 | 5 | ### Patch Changes 6 | 7 | - [`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - fix: remove the duplicates FloatingLayout 8 | 9 | ## 0.1.9 10 | 11 | ### Patch Changes 12 | 13 | - [`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore(yakite-toast): add arm support 14 | 15 | ## 0.1.8 16 | 17 | ### Patch Changes 18 | 19 | - [`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: enable file log 20 | 21 | ## 0.1.7 22 | 23 | ### Patch Changes 24 | 25 | - [`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - feat: fuck github action 26 | 27 | ## 0.1.6 28 | 29 | ### Patch Changes 30 | 31 | - [`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: use deploy key instead of a PAT. 32 | 33 | ## 0.1.5 34 | 35 | ### Patch Changes 36 | 37 | - [`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: try to trigger github action tags monitoring 38 | 39 | ## 0.1.4 40 | 41 | ### Patch Changes 42 | 43 | - [`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: hit github action more accurately 44 | 45 | ## 0.1.3 46 | 47 | ### Patch Changes 48 | 49 | - [`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: release crates and use @changesets/changelog-github 50 | 51 | ## 0.1.2 52 | 53 | ### Patch Changes 54 | 55 | - 1e7cc3b: chore: update zeromq from 6.0.0-beta.17 to 6.0.0-beta.19 56 | 57 | ## 0.1.1 58 | 59 | ### Patch Changes 60 | 61 | - 1c80812: feat: first release 62 | -------------------------------------------------------------------------------- /apps/yakite-yabai/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # yakite-yabai 2 | 3 | ## 0.1.10 4 | 5 | ### Patch Changes 6 | 7 | - [`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - fix: remove the duplicates FloatingLayout 8 | 9 | ## 0.1.9 10 | 11 | ### Patch Changes 12 | 13 | - [`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore(yakite-toast): add arm support 14 | 15 | ## 0.1.8 16 | 17 | ### Patch Changes 18 | 19 | - [`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: enable file log 20 | 21 | ## 0.1.7 22 | 23 | ### Patch Changes 24 | 25 | - [`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - feat: fuck github action 26 | 27 | ## 0.1.6 28 | 29 | ### Patch Changes 30 | 31 | - [`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: use deploy key instead of a PAT. 32 | 33 | ## 0.1.5 34 | 35 | ### Patch Changes 36 | 37 | - [`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: try to trigger github action tags monitoring 38 | 39 | ## 0.1.4 40 | 41 | ### Patch Changes 42 | 43 | - [`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: hit github action more accurately 44 | 45 | ## 0.1.3 46 | 47 | ### Patch Changes 48 | 49 | - [`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: release crates and use @changesets/changelog-github 50 | 51 | ## 0.1.2 52 | 53 | ### Patch Changes 54 | 55 | - 1e7cc3b: chore: update zeromq from 6.0.0-beta.17 to 6.0.0-beta.19 56 | 57 | ## 0.1.1 58 | 59 | ### Patch Changes 60 | 61 | - 1c80812: feat: first release 62 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/stair.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type WindowsLayout } from '.' 7 | 8 | import { WindowState, type IEngineWindow } from '../window' 9 | 10 | import { 11 | type Action, 12 | DecreaseMasterAreaWindowCount, 13 | IncreaseMasterAreaWindowCount 14 | } from '../../action' 15 | 16 | import { Rect } from '../../utils/rect' 17 | import { type IWindowsManager } from '../../manager' 18 | import { type IEngine } from '..' 19 | 20 | export default class StairLayout implements WindowsLayout { 21 | public static readonly id = 'StairLayout' 22 | public readonly classID = StairLayout.id 23 | public readonly name = 'Stair Layout' 24 | public readonly icon = 'bismuth-stair' 25 | 26 | private space: number /* in PIXELS */ 27 | 28 | constructor () { 29 | this.space = 24 30 | } 31 | 32 | public apply ( 33 | _controller: IWindowsManager, 34 | tileables: IEngineWindow[], 35 | area: Rect 36 | ): void { 37 | /* Tile all tileables */ 38 | tileables.forEach((tileable) => (tileable.state = WindowState.Tiled)) 39 | const tiles = tileables 40 | 41 | const len = tiles.length 42 | const space = this.space 43 | 44 | // TODO: limit the maximum number of staired windows. 45 | 46 | for (let i = 0; i < len; i++) { 47 | const dx = space * (len - i - 1) 48 | const dy = space * i 49 | tiles[i].geometry = new Rect( 50 | area.x + dx, 51 | area.y + dy, 52 | area.width - dx, 53 | area.height - dy 54 | ) 55 | } 56 | } 57 | 58 | public clone (): WindowsLayout { 59 | const other = new StairLayout() 60 | other.space = this.space 61 | return other 62 | } 63 | 64 | public executeAction (_engine: IEngine, action: Action): void { 65 | if (action instanceof DecreaseMasterAreaWindowCount) { 66 | // TODO: define arbitrary constants 67 | this.space = Math.max(16, this.space - 8) 68 | } else if (action instanceof IncreaseMasterAreaWindowCount) { 69 | // TODO: define arbitrary constants 70 | this.space = Math.min(160, this.space + 8) 71 | } else { 72 | action.executeWithoutLayoutOverride() 73 | } 74 | } 75 | 76 | public toString (): string { 77 | return `StairLayout(${this.space})` 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.0", 4 | "private": "true", 5 | "type": "module", 6 | "description": "A dynamic tiled window management that bridges the gap between yabai and krohnkite", 7 | "author": "I-Want-ToBelieve", 8 | "license": "MIT", 9 | "scripts": { 10 | "setup": "rm -rf node_modules apps/*/node_modules && pnpm install && pnpm build", 11 | "build": "pnpm clean && pnpm -r build", 12 | "rebuild": "pnpm install && pnpm build", 13 | "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./", 14 | "watch": "pnpm --parallel run watch", 15 | "clean": "rm -rf apps/*/dist", 16 | "changeset": "changeset", 17 | "version:no-npm": "pnpm exec tsx ./scripts/changesets-extra.ts", 18 | "version": "changeset version && pnpm install:frozen && pnpm version:no-npm", 19 | "install:frozen": "pnpm install --frozen-lockfile false", 20 | "release": "changeset publish", 21 | "release:beta": "changeset pre enter beta && pnpm run version && pnpm build && pnpm release && changeset pre exit", 22 | "release:snapshot": "changeset version --snapshot canary && pnpm install:frozen && pnpm build && pnpm release --tag canary --no-git-tag --snapshot" 23 | }, 24 | "keywords": [ 25 | "yakite", 26 | "yabai", 27 | "krohnkite", 28 | "dynamic tiled window management" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/I-Want-ToBelieve/yakite" 33 | }, 34 | "homepage": "https://github.com/I-Want-ToBelieve/yakite", 35 | "bugs": "https://github.com/I-Want-ToBelieve/yakite/issues", 36 | "dependencies": { 37 | "@changesets/changelog-github": "^0.4.8", 38 | "@changesets/cli": "^2.26.2", 39 | "@iarna/toml": "^2.2.5", 40 | "@manypkg/cli": "^0.21.0", 41 | "@manypkg/get-packages": "^2.2.0", 42 | "@microsoft/api-extractor": "^7.38.3", 43 | "@types/node": "latest", 44 | "@typescript-eslint/eslint-plugin": "^6.11.0", 45 | "bun-types": "^1.0.13", 46 | "eslint": "^8.54.0", 47 | "eslint-config-standard-with-typescript": "latest", 48 | "eslint-plugin-import": "^2.29.0", 49 | "eslint-plugin-n": "^16.3.1", 50 | "eslint-plugin-promise": "^6.1.1", 51 | "execa": "^8.0.1", 52 | "parse-json": "^8.0.0", 53 | "prettier": "^3.1.0", 54 | "ts-json-schema-generator": "^1.4.0", 55 | "ts-standard": "^12.0.2", 56 | "ts-to-zod": "^3.2.0", 57 | "tsc-alias": "^1.8.8", 58 | "tsup": "^8.0.0", 59 | "tsx": "^4.2.0", 60 | "zod": "^3.22.4" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/spread.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type WindowsLayout } from '.' 7 | 8 | import { WindowState, type IEngineWindow } from '../window' 9 | 10 | import { 11 | type Action, 12 | DecreaseMasterAreaWindowCount, 13 | IncreaseMasterAreaWindowCount 14 | } from '../../action' 15 | 16 | import { Rect } from '../../utils/rect' 17 | import { type IWindowsManager } from '../../manager' 18 | import { type IEngine } from '..' 19 | 20 | export default class SpreadLayout implements WindowsLayout { 21 | public static readonly id = 'SpreadLayout' 22 | public readonly classID = SpreadLayout.id 23 | public readonly name = 'Spread Layout' 24 | public readonly icon = 'bismuth-spread' 25 | 26 | private space: number /* in ratio */ 27 | 28 | constructor () { 29 | this.space = 0.07 30 | } 31 | 32 | public apply ( 33 | _controller: IWindowsManager, 34 | tileables: IEngineWindow[], 35 | area: Rect 36 | ): void { 37 | /* Tile all tileables */ 38 | tileables.forEach((tileable) => (tileable.state = WindowState.Tiled)) 39 | const tiles = tileables 40 | 41 | let numTiles = tiles.length 42 | const spaceWidth = Math.floor(area.width * this.space) 43 | let cardWidth = area.width - spaceWidth * (numTiles - 1) 44 | 45 | // TODO: define arbitrary constants 46 | const miniumCardWidth = area.width * 0.4 47 | while (cardWidth < miniumCardWidth) { 48 | cardWidth += spaceWidth 49 | numTiles -= 1 50 | } 51 | 52 | for (let i = 0; i < tiles.length; i++) { 53 | tiles[i].geometry = new Rect( 54 | area.x + (i < numTiles ? spaceWidth * (numTiles - i - 1) : 0), 55 | area.y, 56 | cardWidth, 57 | area.height 58 | ) 59 | } 60 | } 61 | 62 | public clone (): WindowsLayout { 63 | const other = new SpreadLayout() 64 | other.space = this.space 65 | return other 66 | } 67 | 68 | public executeAction (_engine: IEngine, action: Action): void { 69 | if (action instanceof DecreaseMasterAreaWindowCount) { 70 | // TODO: define arbitrary constants 71 | this.space = Math.max(0.04, this.space - 0.01) 72 | } else if (action instanceof IncreaseMasterAreaWindowCount) { 73 | // TODO: define arbitrary constants 74 | this.space = Math.min(0.1, this.space + 0.01) 75 | } else { 76 | action.executeWithoutLayoutOverride() 77 | } 78 | } 79 | 80 | public toString (): string { 81 | return `SpreadLayout(${this.space})` 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/bridge/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type IBridgeSurface } from './surface' 7 | 8 | import { type ActionNames } from '@/action' 9 | import { type WindowsManager, type EventNames, type WindowEventCallbacks } from '@/manager' 10 | import { type IEngineWindow } from '@/engine/window' 11 | 12 | /** 13 | * The responsibility of Bridge is to provide the operations provided by the window manager to the actual caller, 14 | * and to hand over the triggerable event listeners to the actual triggerer for binding 15 | * As its name suggests, it is a bridge between the running environment and the window manager layout engine.. 16 | */ 17 | export interface IBridge { 18 | wm?: WindowsManager 19 | /** 20 | * All the surfaces/screens currently possess by the running environment 21 | */ 22 | readonly screens: IBridgeSurface[] 23 | 24 | /** 25 | * Surface (screen) of the current window 26 | */ 27 | currentSurface: IBridgeSurface 28 | 29 | /** 30 | * Currently active (i.e. focused) window 31 | */ 32 | currentWindow: IEngineWindow | null 33 | 34 | /** 35 | * Events that the window manager needs to listen for 36 | */ 37 | listeners: { 38 | [K in EventNames]: WindowEventCallbacks[K] 39 | } 40 | 41 | /** 42 | * Actions that can be performed on the window manager 43 | */ 44 | actions: Record any> | Record 45 | 46 | /** 47 | * Show a popup notification in the center of the screen. 48 | * @param text the main text of the notification. 49 | * @param icon an optional name of the icon to display in the pop-up. 50 | * @param hint an optional string displayed beside the main text. 51 | */ 52 | showNotification: (text: string, icon?: string, hint?: string) => void 53 | 54 | /** 55 | * The window manager needs to listen to events in the environment, 56 | * so it applies to the bridge to add these events, 57 | * and then the other side of the bridge calls the corresponding callback function when the relevant events occur. 58 | */ 59 | addEventListener: (name: K, callback: WindowEventCallbacks[K]) => void 60 | 61 | /** 62 | * The window manager exposes actions to the bridge, 63 | * so that the other side of the bridge can call those actions through the bridge., 64 | */ 65 | addAction: (name: ActionNames, callback: () => any) => void 66 | 67 | /** 68 | * Manage the windows, that were active before script loading 69 | */ 70 | manageWindows: () => void 71 | 72 | /** 73 | * Destroy all callbacks and other non-GC resources 74 | */ 75 | drop: () => void 76 | } 77 | 78 | export * from './surface' 79 | export * from './window' 80 | -------------------------------------------------------------------------------- /examples/services.nix: -------------------------------------------------------------------------------- 1 | {...}: let 2 | # yakite = "/Users/i.want.to.believe/git.workspaces/js.workspaces/yakite/apps/yakite/target/release/yakite"; 3 | yakite = "yakite"; 4 | in { 5 | services = { 6 | karabiner-elements = { 7 | enable = true; 8 | }; 9 | 10 | skhd = { 11 | enable = true; 12 | skhdConfig = '' 13 | ctrl - return : kitty --single-instance -d ~ 14 | 15 | cmd - backspace : skhd -k "ctrl - space" 16 | 17 | ctrl - w : skhd -k "ctrl - up" 18 | 19 | ctrl - 1 : yabai -m space --focus 1 20 | ctrl - 2 : yabai -m space --focus 2 21 | ctrl - 3 : yabai -m space --focus 3 22 | ctrl - 4 : yabai -m space --focus 4 23 | ctrl - 5 : yabai -m space --focus 5 24 | ctrl - 6 : yabai -m space --focus 6 25 | ctrl - 7 : yabai -m space --focus 7 26 | ctrl - 8 : yabai -m space --focus 8 27 | ctrl - 9 : yabai -m space --focus 9 28 | ctrl - 0 : yabai -m space --focus 10 29 | 30 | ctrl + shift - j : yabai -m window --space prev 31 | ctrl + shift - l : yabai -m window --space next 32 | ctrl + shift - 1 : yabai -m window --space 1 33 | ctrl + shift - 2 : yabai -m window --space 2 34 | ctrl + shift - 3 : yabai -m window --space 3 35 | ctrl + shift - 4 : yabai -m window --space 4 36 | ctrl + shift - 5 : yabai -m window --space 5 37 | ctrl + shift - 6 : yabai -m window --space 6 38 | ctrl + shift - 7 : yabai -m window --space 7 39 | ctrl + shift - 8 : yabai -m window --space 8 40 | ctrl + shift - 9 : yabai -m window --space 9 41 | ctrl + shift - 0 : yabai -m window --space 10 42 | 43 | 44 | ctrl - q : yabai -m window --close 45 | ctrl - x : yabai -m window --minimize 46 | 47 | ctrl - t : ${yakite} action toggle-tile-layout 48 | ctrl - m : ${yakite} action toggle-monocle-layout 49 | ctrl - f : ${yakite} action toggle-active-window-floating 50 | ctrl - k : ${yakite} action focus-next-window 51 | ctrl - i : ${yakite} action focus-previous-window 52 | ctrl + shift - k : ${yakite} action move-active-window-to-next-position 53 | ctrl + shift - i : ${yakite} action move-active-window-to-previous-position 54 | ctrl + shift - m : ${yakite} action push-active-window-into-master-area-front 55 | 56 | ctrl - 0x2A : ${yakite} action switch-to-next-layout 57 | 58 | ctrl - j : ${yakite} action decrease-layout-master-area-size 59 | ctrl - l : ${yakite} action increase-layout-master-area-size 60 | ''; 61 | }; 62 | 63 | yabai = { 64 | enable = true; 65 | enableScriptingAddition = true; 66 | config = { 67 | focus_follows_mouse = "off"; 68 | mouse_follows_focus = "off"; 69 | window_opacity = 0.90; 70 | }; 71 | extraConfig = '' 72 | borders active_color=0xff6eff89 inactive_color=0xff516468 width=12.0 2>/dev/null 1>&2 & 73 | 74 | yakite-daemon 2>/dev/null 1>&2 & 75 | ''; 76 | }; 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/spiral.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { HalfSplitLayoutPart, FillLayoutPart } from './part' 7 | import { type WindowsLayout } from '.' 8 | 9 | import { WindowState, type IEngineWindow } from '../window' 10 | 11 | import { type Rect, type RectDelta } from '@/utils/rect' 12 | 13 | import { type Config } from '@/config' 14 | import { type IWindowsManager } from '@/manager' 15 | 16 | export type SpiralLayoutPart = HalfSplitLayoutPart< 17 | FillLayoutPart, 18 | SpiralLayoutPart | FillLayoutPart 19 | > 20 | 21 | export default class SpiralLayout implements WindowsLayout { 22 | public static readonly id = 'SpiralLayout' 23 | public readonly classID = SpiralLayout.id 24 | public readonly name = 'Spiral Layout' 25 | public readonly icon = 'bismuth-spiral' 26 | 27 | private depth: number 28 | private readonly parts: SpiralLayoutPart 29 | 30 | private readonly config: Config 31 | 32 | constructor (config: Config) { 33 | this.config = config 34 | 35 | this.depth = 1 36 | this.parts = new HalfSplitLayoutPart( 37 | new FillLayoutPart(), 38 | new FillLayoutPart() 39 | ) 40 | this.parts.angle = 0 41 | this.parts.gap = this.config.gaps.tileLayout 42 | } 43 | 44 | public adjust ( 45 | area: Rect, 46 | tiles: IEngineWindow[], 47 | basis: IEngineWindow, 48 | delta: RectDelta 49 | ): void { 50 | this.parts.adjust(area, tiles, basis, delta) 51 | } 52 | 53 | public apply ( 54 | _controller: IWindowsManager, 55 | tileables: IEngineWindow[], 56 | area: Rect 57 | ): void { 58 | tileables.forEach((tileable) => (tileable.state = WindowState.Tiled)) 59 | 60 | this.bore(tileables.length) 61 | 62 | this.parts.apply(area, tileables).forEach((geometry, i) => { 63 | tileables[i].geometry = geometry 64 | }) 65 | } 66 | 67 | // handleShortcut?(ctx: EngineContext, input: Shortcut, data?: any): boolean; 68 | 69 | public toString (): string { 70 | return 'Spiral()' 71 | } 72 | 73 | private bore (depth: number): void { 74 | if (this.depth >= depth) { 75 | return 76 | } 77 | 78 | let hpart = this.parts 79 | let i 80 | for (i = 0; i < this.depth - 1; i++) { 81 | hpart = hpart.secondary as SpiralLayoutPart 82 | } 83 | 84 | const lastFillPart = hpart.secondary as FillLayoutPart 85 | let npart: SpiralLayoutPart 86 | while (i < depth - 1) { 87 | npart = new HalfSplitLayoutPart(new FillLayoutPart(), lastFillPart) 88 | npart.gap = this.config.gaps.tileLayout 89 | switch ((i + 1) % 4) { 90 | case 0: 91 | npart.angle = 0 92 | break 93 | case 1: 94 | npart.angle = 90 95 | break 96 | case 2: 97 | npart.angle = 180 98 | break 99 | case 3: 100 | npart.angle = 270 101 | break 102 | } 103 | 104 | hpart.secondary = npart 105 | hpart = npart 106 | i++ 107 | } 108 | this.depth = depth 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /scripts/changesets-extra.ts: -------------------------------------------------------------------------------- 1 | import { getPackages } from '@manypkg/get-packages' 2 | import path from 'node:path' 3 | import fs from 'node:fs/promises' 4 | import parseJson from 'parse-json' 5 | import toml from '@iarna/toml' 6 | import * as prettier from 'prettier' 7 | 8 | export type ILanguage = 'objective-c' | 'rust' 9 | export interface IPackageJson { 10 | changesetsExtra: { 11 | language: ILanguage 12 | sources: string[] 13 | versionUpdatePolicy: 'source-code-replacement' | 'auto' 14 | prevVersion?: string 15 | } 16 | } 17 | 18 | const { packages } = await getPackages(process.cwd()) 19 | 20 | // console.dir( 21 | // { packages }, 22 | // { 23 | // depth: Number.POSITIVE_INFINITY, 24 | // colors: true 25 | // } 26 | // ) 27 | 28 | packages 29 | .filter( 30 | it => 31 | 'changesetsExtra' in 32 | (it.packageJson as typeof it.packageJson & 33 | IPackageJson) 34 | ) 35 | .forEach(it => { 36 | const { changesetsExtra, version } = 37 | it.packageJson as typeof it.packageJson & IPackageJson 38 | 39 | const { 40 | language, 41 | sources, 42 | versionUpdatePolicy, 43 | prevVersion 44 | } = changesetsExtra 45 | const { dir } = it 46 | 47 | switch (language) { 48 | case 'rust': 49 | if (versionUpdatePolicy === 'auto') { 50 | ;(async () => { 51 | const cargoPath = path.join(dir, 'Cargo.toml') 52 | 53 | const cargoToml = toml.parse(String(await fs.readFile(cargoPath))) as any 54 | 55 | cargoToml.package.version = version 56 | 57 | await fs.writeFile(cargoPath, toml.stringify(cargoToml)) 58 | })().catch((e) => { 59 | console.error(e) 60 | }) 61 | } 62 | break 63 | case 'objective-c': 64 | if ( 65 | versionUpdatePolicy === 'source-code-replacement' 66 | ) { 67 | if (!prevVersion) { 68 | throw new Error( 69 | `Not found prevVersion field in ${dir}/package.json changesetsExtra prop` 70 | ) 71 | } 72 | 73 | sources 74 | .map(it => path.join(dir, it)) 75 | .forEach(it => { 76 | ;(async () => { 77 | const source = await fs.readFile(it) 78 | await fs.writeFile( 79 | it, 80 | String(source).replaceAll( 81 | prevVersion, 82 | version 83 | ) 84 | ) 85 | 86 | const packageJsonPath = path.join( 87 | dir, 88 | 'package.json' 89 | ) 90 | const packageJson = parseJson( 91 | ( 92 | await fs.readFile(packageJsonPath) 93 | ).toString() 94 | ) as any 95 | packageJson.changesetsExtra.prevVersion = 96 | version 97 | await fs.writeFile( 98 | packageJsonPath, 99 | await prettier.format(JSON.stringify(packageJson), { parser: 'json' }) 100 | ) 101 | })().catch(e => { 102 | console.error(e) 103 | }) 104 | }) 105 | } 106 | break 107 | default: 108 | break 109 | } 110 | }) 111 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/utils/func.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | /** 7 | * Return `value`, if it is in range of [`min`, `max`]. Otherwise return 8 | * the the closest range ends to it. 9 | */ 10 | export function clip (value: number, min: number, max: number): number { 11 | if (value < min) { 12 | return min 13 | } 14 | if (value > max) { 15 | return max 16 | } 17 | return value 18 | } 19 | 20 | /** 21 | * Modify the value gradually, multiplying it by (step / (1 + step)) 22 | */ 23 | export function slide (value: number, step: number): number { 24 | if (step === 0) { 25 | return value 26 | } 27 | return Math.floor(value / step + 1.000001) * step 28 | } 29 | 30 | /** 31 | * Find the words in the string. 32 | * @param str the string to search words in 33 | * @param words the words to be searched 34 | * @returns index of a word, that was found in the string first. -1 if none of the words were found. 35 | */ 36 | export function matchWords (str: string, words: string[]): number { 37 | for (let i = 0; i < words.length; i++) { 38 | if (str.includes(words[i])) { 39 | return i 40 | } 41 | } 42 | return -1 43 | } 44 | 45 | export function wrapIndex (index: number, length: number): number { 46 | if (index < 0) { 47 | return index + length 48 | } 49 | if (index >= length) { 50 | return index - length 51 | } 52 | return index 53 | } 54 | 55 | /** 56 | * Partition the given array into two parts, based on the value of the predicate 57 | * 58 | * @param array 59 | * @param predicate A function which accepts an item and returns a boolean value. 60 | * @return A tuple containing an array of true(matched) items, and an array of false(unmatched) items. 61 | */ 62 | export function partitionArray ( 63 | array: T[], 64 | predicate: (item: T, index: number) => boolean 65 | ): [T[], T[]] { 66 | return array.reduce( 67 | (parts: [T[], T[]], item: T, index: number) => { 68 | parts[predicate(item, index) ? 0 : 1].push(item) 69 | return parts 70 | }, 71 | [[], []] 72 | ) 73 | } 74 | 75 | /** 76 | * Partition the array into chunks of designated sizes. 77 | * 78 | * This function splits the given array into N+1 chunks, where N chunks are 79 | * specified by `sizes`, and the additional chunk is for remaining items. When 80 | * the array runs out of items first, any remaining chunks will be empty. 81 | * @param array 82 | * @param sizes A list of chunk sizes 83 | * @returns An array of (N+1) chunks, where the last chunk contains remaining 84 | * items. 85 | */ 86 | export function partitionArrayBySizes (array: T[], sizes: number[]): T[][] { 87 | let base = 0 88 | const chunks = sizes.map((size): T[] => { 89 | const chunk = array.slice(base, base + size) 90 | base += size 91 | return chunk 92 | }) 93 | chunks.push(array.slice(base)) 94 | 95 | return chunks 96 | } 97 | 98 | /** 99 | * Tests if two ranges are overlapping 100 | * @param min1 Range 1, begin 101 | * @param max1 Range 1, end 102 | * @param min2 Range 2, begin 103 | * @param max2 Range 2, end 104 | */ 105 | export function overlap ( 106 | min1: number, 107 | max1: number, 108 | min2: number, 109 | max2: number 110 | ): boolean { 111 | const min = Math.min 112 | const max = Math.max 113 | const dx = max(0, min(max1, max2) - max(min1, min2)) 114 | return dx > 0 115 | } 116 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/cascade.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type WindowsLayout } from '.' 7 | 8 | import { type IEngine } from '..' 9 | import { WindowState, type IEngineWindow } from '../window' 10 | 11 | import { 12 | type Action, 13 | DecreaseMasterAreaWindowCount, 14 | IncreaseMasterAreaWindowCount 15 | } from '../../action' 16 | import { type IWindowsManager } from '../../manager' 17 | 18 | import { Rect } from '../../utils/rect' 19 | 20 | export enum CascadeDirection { 21 | NorthWest = 0, 22 | North = 1, 23 | NorthEast = 2, 24 | East = 3, 25 | SouthEast = 4, 26 | South = 5, 27 | SouthWest = 6, 28 | West = 7, 29 | } 30 | 31 | export default class CascadeLayout implements WindowsLayout { 32 | public static readonly id = 'CascadeLayout' 33 | public readonly classID = CascadeLayout.id 34 | public readonly name = 'Cascade Layout' 35 | public readonly icon = 'bismuth-cascade' 36 | 37 | /** Decompose direction into vertical and horizontal steps */ 38 | public static decomposeDirection ( 39 | dir: CascadeDirection 40 | ): [-1 | 0 | 1, -1 | 0 | 1] { 41 | switch (dir) { 42 | case CascadeDirection.NorthWest: 43 | return [-1, -1] 44 | case CascadeDirection.North: 45 | return [-1, 0] 46 | case CascadeDirection.NorthEast: 47 | return [-1, 1] 48 | case CascadeDirection.East: 49 | return [0, 1] 50 | case CascadeDirection.SouthEast: 51 | return [1, 1] 52 | case CascadeDirection.South: 53 | return [1, 0] 54 | case CascadeDirection.SouthWest: 55 | return [1, -1] 56 | case CascadeDirection.West: 57 | return [0, -1] 58 | } 59 | } 60 | 61 | public get hint (): string { 62 | return String(CascadeDirection[this.dir]) 63 | } 64 | 65 | constructor (private dir: CascadeDirection = CascadeDirection.SouthEast) { 66 | /* nothing */ 67 | } 68 | 69 | public apply ( 70 | _controller: IWindowsManager, 71 | tileables: IEngineWindow[], 72 | area: Rect 73 | ): void { 74 | const [vertStep, horzStep] = CascadeLayout.decomposeDirection(this.dir) 75 | 76 | // TODO: adjustable step size 77 | const stepSize = 25 78 | 79 | const windowWidth = 80 | horzStep !== 0 81 | ? area.width - stepSize * (tileables.length - 1) 82 | : area.width 83 | const windowHeight = 84 | vertStep !== 0 85 | ? area.height - stepSize * (tileables.length - 1) 86 | : area.height 87 | 88 | const baseX = horzStep >= 0 ? area.x : area.maxX - windowWidth 89 | const baseY = vertStep >= 0 ? area.y : area.maxY - windowHeight 90 | 91 | let x = baseX 92 | let y = baseY 93 | tileables.forEach((tile) => { 94 | tile.state = WindowState.Tiled 95 | tile.geometry = new Rect(x, y, windowWidth, windowHeight) 96 | 97 | x += horzStep * stepSize 98 | y += vertStep * stepSize 99 | }) 100 | } 101 | 102 | public clone (): CascadeLayout { 103 | return new CascadeLayout(this.dir) 104 | } 105 | 106 | public executeAction (engine: IEngine, action: Action): void { 107 | if (action instanceof IncreaseMasterAreaWindowCount) { 108 | this.dir = (this.dir + 1 + 8) % 8 109 | engine.showLayoutNotification() 110 | } else if (action instanceof DecreaseMasterAreaWindowCount) { 111 | this.dir = (this.dir - 1 + 8) % 8 112 | engine.showLayoutNotification() 113 | } else { 114 | action.executeWithoutLayoutOverride() 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .devenv 2 | .pre-commit-config.yaml 3 | 4 | # Created by https://www.toptal.com/developers/gitignore/api/node,direnv,rust 5 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,direnv,rust 6 | 7 | ### direnv ### 8 | .direnv 9 | 10 | ### Node ### 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | lerna-debug.log* 18 | .pnpm-debug.log* 19 | 20 | # Diagnostic reports (https://nodejs.org/api/report.html) 21 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | *.pid.lock 28 | 29 | # Directory for instrumented libs generated by jscoverage/JSCover 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | coverage 34 | *.lcov 35 | 36 | # nyc test coverage 37 | .nyc_output 38 | 39 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 40 | .grunt 41 | 42 | # Bower dependency directory (https://bower.io/) 43 | bower_components 44 | 45 | # node-waf configuration 46 | .lock-wscript 47 | 48 | # Compiled binary addons (https://nodejs.org/api/addons.html) 49 | build/Release 50 | 51 | # Dependency directories 52 | node_modules/ 53 | jspm_packages/ 54 | 55 | # Snowpack dependency directory (https://snowpack.dev/) 56 | web_modules/ 57 | 58 | # TypeScript cache 59 | *.tsbuildinfo 60 | 61 | # Optional npm cache directory 62 | .npm 63 | 64 | # Optional eslint cache 65 | .eslintcache 66 | 67 | # Optional stylelint cache 68 | .stylelintcache 69 | 70 | # Microbundle cache 71 | .rpt2_cache/ 72 | .rts2_cache_cjs/ 73 | .rts2_cache_es/ 74 | .rts2_cache_umd/ 75 | 76 | # Optional REPL history 77 | .node_repl_history 78 | 79 | # Output of 'npm pack' 80 | *.tgz 81 | 82 | # Yarn Integrity file 83 | .yarn-integrity 84 | 85 | # dotenv environment variable files 86 | .env 87 | .env.development.local 88 | .env.test.local 89 | .env.production.local 90 | .env.local 91 | 92 | # parcel-bundler cache (https://parceljs.org/) 93 | .cache 94 | .parcel-cache 95 | 96 | # Next.js build output 97 | .next 98 | out 99 | 100 | # Nuxt.js build / generate output 101 | .nuxt 102 | dist 103 | 104 | # Gatsby files 105 | .cache/ 106 | # Comment in the public line in if your project uses Gatsby and not Next.js 107 | # https://nextjs.org/blog/next-9-1#public-directory-support 108 | # public 109 | 110 | # vuepress build output 111 | .vuepress/dist 112 | 113 | # vuepress v2.x temp and cache directory 114 | .temp 115 | 116 | # Docusaurus cache and generated files 117 | .docusaurus 118 | 119 | # Serverless directories 120 | .serverless/ 121 | 122 | # FuseBox cache 123 | .fusebox/ 124 | 125 | # DynamoDB Local files 126 | .dynamodb/ 127 | 128 | # TernJS port file 129 | .tern-port 130 | 131 | # Stores VSCode versions used for testing VSCode extensions 132 | .vscode-test 133 | 134 | # yarn v2 135 | .yarn/cache 136 | .yarn/unplugged 137 | .yarn/build-state.yml 138 | .yarn/install-state.gz 139 | .pnp.* 140 | 141 | ### Node Patch ### 142 | # Serverless Webpack directories 143 | .webpack/ 144 | 145 | # Optional stylelint cache 146 | 147 | # SvelteKit build / generate output 148 | .svelte-kit 149 | 150 | ### Rust ### 151 | # Generated by Cargo 152 | # will have compiled files and executables 153 | debug/ 154 | target/ 155 | 156 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 157 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 158 | Cargo.lock 159 | 160 | # These are backup files generated by rustfmt 161 | **/*.rs.bk 162 | 163 | # MSVC Windows builds of rustc generate these, which store debugging information 164 | *.pdb 165 | 166 | # End of https://www.toptal.com/developers/gitignore/api/node,direnv,rust 167 | -------------------------------------------------------------------------------- /apps/yakite-config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # yakite-config 2 | 3 | ## 0.1.10 4 | 5 | ### Patch Changes 6 | 7 | - [`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - fix: remove the duplicates FloatingLayout 8 | 9 | - Updated dependencies [[`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff)]: 10 | - krohnkite-core@0.1.10 11 | 12 | ## 0.1.9 13 | 14 | ### Patch Changes 15 | 16 | - [`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore(yakite-toast): add arm support 17 | 18 | - Updated dependencies [[`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b)]: 19 | - krohnkite-core@0.1.9 20 | 21 | ## 0.1.8 22 | 23 | ### Patch Changes 24 | 25 | - [`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: enable file log 26 | 27 | - Updated dependencies [[`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba)]: 28 | - krohnkite-core@0.1.8 29 | 30 | ## 0.1.7 31 | 32 | ### Patch Changes 33 | 34 | - [`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - feat: fuck github action 35 | 36 | - Updated dependencies [[`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019)]: 37 | - krohnkite-core@0.1.7 38 | 39 | ## 0.1.6 40 | 41 | ### Patch Changes 42 | 43 | - [`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: use deploy key instead of a PAT. 44 | 45 | - Updated dependencies [[`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f)]: 46 | - krohnkite-core@0.1.6 47 | 48 | ## 0.1.5 49 | 50 | ### Patch Changes 51 | 52 | - [`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: try to trigger github action tags monitoring 53 | 54 | - Updated dependencies [[`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8)]: 55 | - krohnkite-core@0.1.5 56 | 57 | ## 0.1.4 58 | 59 | ### Patch Changes 60 | 61 | - [`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: hit github action more accurately 62 | 63 | - Updated dependencies [[`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910)]: 64 | - krohnkite-core@0.1.4 65 | 66 | ## 0.1.3 67 | 68 | ### Patch Changes 69 | 70 | - [`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: release crates and use @changesets/changelog-github 71 | 72 | - Updated dependencies [[`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b)]: 73 | - krohnkite-core@0.1.3 74 | 75 | ## 0.1.2 76 | 77 | ### Patch Changes 78 | 79 | - 1e7cc3b: chore: update zeromq from 6.0.0-beta.17 to 6.0.0-beta.19 80 | - Updated dependencies [1e7cc3b] 81 | - krohnkite-core@0.1.2 82 | 83 | ## 0.1.1 84 | 85 | ### Patch Changes 86 | 87 | - 1c80812: feat: first release 88 | - Updated dependencies [1c80812] 89 | - krohnkite-core@0.1.1 90 | -------------------------------------------------------------------------------- /apps/yakite-config/src/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$ref": "#/definitions/Config", 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "definitions": { 5 | "Config": { 6 | "additionalProperties": false, 7 | "properties": { 8 | "experimentalBackend": { 9 | "type": "boolean" 10 | }, 11 | "floating": { 12 | "additionalProperties": false, 13 | "properties": { 14 | "class": { 15 | "items": { 16 | "type": "string" 17 | }, 18 | "type": "array" 19 | }, 20 | "title": { 21 | "items": { 22 | "type": "string" 23 | }, 24 | "type": "array" 25 | } 26 | }, 27 | "required": [ 28 | "class", 29 | "title" 30 | ], 31 | "type": "object" 32 | }, 33 | "gaps": { 34 | "additionalProperties": false, 35 | "properties": { 36 | "screen": { 37 | "additionalProperties": false, 38 | "properties": { 39 | "bottom": { 40 | "type": "number" 41 | }, 42 | "left": { 43 | "type": "number" 44 | }, 45 | "right": { 46 | "type": "number" 47 | }, 48 | "top": { 49 | "type": "number" 50 | } 51 | }, 52 | "required": [ 53 | "top", 54 | "left", 55 | "right", 56 | "bottom" 57 | ], 58 | "type": "object" 59 | }, 60 | "tileLayout": { 61 | "type": "number" 62 | } 63 | }, 64 | "required": [ 65 | "screen", 66 | "tileLayout" 67 | ], 68 | "type": "object" 69 | }, 70 | "ignore": { 71 | "additionalProperties": false, 72 | "properties": { 73 | "class": { 74 | "items": { 75 | "type": "string" 76 | }, 77 | "type": "array" 78 | }, 79 | "screen": { 80 | "items": { 81 | "type": "number" 82 | }, 83 | "type": "array" 84 | }, 85 | "title": { 86 | "items": { 87 | "type": "string" 88 | }, 89 | "type": "array" 90 | } 91 | }, 92 | "required": [ 93 | "class", 94 | "title", 95 | "screen" 96 | ], 97 | "type": "object" 98 | }, 99 | "keepFloatAbove": { 100 | "type": "boolean" 101 | }, 102 | "layoutOrder": { 103 | "items": { 104 | "type": "string" 105 | }, 106 | "type": "array" 107 | }, 108 | "limitTileWidthRatio": { 109 | "type": "number" 110 | }, 111 | "maximizeSoleTile": { 112 | "type": "boolean" 113 | }, 114 | "monocleMaximize": { 115 | "type": "boolean" 116 | }, 117 | "monocleMinimizeRest": { 118 | "type": "boolean" 119 | }, 120 | "newWindowAsMaster": { 121 | "type": "boolean" 122 | }, 123 | "noTileBorder": { 124 | "type": "boolean" 125 | }, 126 | "untileByDragging": { 127 | "type": "boolean" 128 | } 129 | }, 130 | "required": [ 131 | "experimentalBackend", 132 | "layoutOrder", 133 | "monocleMaximize", 134 | "maximizeSoleTile", 135 | "monocleMinimizeRest", 136 | "untileByDragging", 137 | "keepFloatAbove", 138 | "noTileBorder", 139 | "limitTileWidthRatio", 140 | "gaps", 141 | "newWindowAsMaster", 142 | "ignore", 143 | "floating" 144 | ], 145 | "type": "object" 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /apps/yakite-bridge/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # yakite-bridge 2 | 3 | ## 0.1.10 4 | 5 | ### Patch Changes 6 | 7 | - [`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - fix: remove the duplicates FloatingLayout 8 | 9 | - Updated dependencies [[`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff)]: 10 | - krohnkite-core@0.1.10 11 | - yakite-config@0.1.10 12 | - yakite-message@0.1.10 13 | - yakite-yabai@0.1.10 14 | 15 | ## 0.1.9 16 | 17 | ### Patch Changes 18 | 19 | - [`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore(yakite-toast): add arm support 20 | 21 | - Updated dependencies [[`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b)]: 22 | - krohnkite-core@0.1.9 23 | - yakite-config@0.1.9 24 | - yakite-message@0.1.9 25 | - yakite-yabai@0.1.9 26 | 27 | ## 0.1.8 28 | 29 | ### Patch Changes 30 | 31 | - [`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: enable file log 32 | 33 | - Updated dependencies [[`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba)]: 34 | - krohnkite-core@0.1.8 35 | - yakite-config@0.1.8 36 | - yakite-message@0.1.8 37 | - yakite-yabai@0.1.8 38 | 39 | ## 0.1.7 40 | 41 | ### Patch Changes 42 | 43 | - [`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - feat: fuck github action 44 | 45 | - Updated dependencies [[`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019)]: 46 | - krohnkite-core@0.1.7 47 | - yakite-config@0.1.7 48 | - yakite-message@0.1.7 49 | - yakite-yabai@0.1.7 50 | 51 | ## 0.1.6 52 | 53 | ### Patch Changes 54 | 55 | - [`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: use deploy key instead of a PAT. 56 | 57 | - Updated dependencies [[`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f)]: 58 | - krohnkite-core@0.1.6 59 | - yakite-config@0.1.6 60 | - yakite-message@0.1.6 61 | - yakite-yabai@0.1.6 62 | 63 | ## 0.1.5 64 | 65 | ### Patch Changes 66 | 67 | - [`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: try to trigger github action tags monitoring 68 | 69 | - Updated dependencies [[`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8)]: 70 | - krohnkite-core@0.1.5 71 | - yakite-config@0.1.5 72 | - yakite-message@0.1.5 73 | - yakite-yabai@0.1.5 74 | 75 | ## 0.1.4 76 | 77 | ### Patch Changes 78 | 79 | - [`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: hit github action more accurately 80 | 81 | - Updated dependencies [[`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910)]: 82 | - krohnkite-core@0.1.4 83 | - yakite-config@0.1.4 84 | - yakite-message@0.1.4 85 | - yakite-yabai@0.1.4 86 | 87 | ## 0.1.3 88 | 89 | ### Patch Changes 90 | 91 | - [`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: release crates and use @changesets/changelog-github 92 | 93 | - Updated dependencies [[`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b)]: 94 | - krohnkite-core@0.1.3 95 | - yakite-config@0.1.3 96 | - yakite-message@0.1.3 97 | - yakite-yabai@0.1.3 98 | 99 | ## 0.1.2 100 | 101 | ### Patch Changes 102 | 103 | - 1e7cc3b: chore: update zeromq from 6.0.0-beta.17 to 6.0.0-beta.19 104 | - Updated dependencies [1e7cc3b] 105 | - krohnkite-core@0.1.2 106 | - yakite-config@0.1.2 107 | - yakite-message@0.1.2 108 | - yakite-yabai@0.1.2 109 | 110 | ## 0.1.1 111 | 112 | ### Patch Changes 113 | 114 | - 1c80812: feat: first release 115 | - Updated dependencies [1c80812] 116 | - krohnkite-core@0.1.1 117 | - yakite-message@0.1.1 118 | - yakite-config@0.1.1 119 | - yakite-yabai@0.1.1 120 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/store/layout.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import FloatingLayout from '../layouts/floating' 7 | 8 | import { type WindowsLayout } from '../layouts' 9 | 10 | import { type IBridgeSurface } from '../../bridge/surface' 11 | 12 | import { wrapIndex } from '../../utils/func' 13 | 14 | import MonocleLayout from '../layouts/monocle' 15 | import TileLayout from '../layouts/tile' 16 | // import CascadeLayout from './layout/cascade_layout' 17 | import QuarterLayout from '../layouts/quarter' 18 | import SpiralLayout from '../layouts/spiral' 19 | import SpreadLayout from '../layouts/spread' 20 | import StairLayout from '../layouts/stair' 21 | import ThreeColumnLayout from '../layouts/three-column' 22 | import { type Config } from '@/config' 23 | 24 | export class LayoutStoreEntry { 25 | public get currentLayout (): WindowsLayout { 26 | return this.loadLayout(this.currentID) 27 | } 28 | 29 | private currentIndex: number | null 30 | private currentID: string 31 | private layouts: Record 32 | private previousID: string 33 | 34 | private readonly config: Config 35 | 36 | constructor (config: Config) { 37 | this.config = config 38 | this.currentIndex = 0 39 | this.currentID = this.config.layoutOrder[0] 40 | this.layouts = {} 41 | this.previousID = this.currentID 42 | 43 | this.loadLayout(this.currentID) 44 | } 45 | 46 | public cycleLayout (step: -1 | 1): WindowsLayout { 47 | this.previousID = this.currentID 48 | this.currentIndex = 49 | this.currentIndex !== null 50 | ? wrapIndex(this.currentIndex + step, this.config.layoutOrder.length) 51 | : 0 52 | this.currentID = this.config.layoutOrder[this.currentIndex] 53 | return this.loadLayout(this.currentID) 54 | } 55 | 56 | public toggleLayout (targetID: string): WindowsLayout { 57 | const targetLayout = this.loadLayout(targetID) 58 | 59 | // Toggle if requested, set otherwise 60 | if (this.currentID === targetID) { 61 | this.currentID = this.previousID 62 | this.previousID = targetID 63 | } else { 64 | this.previousID = this.currentID 65 | this.currentID = targetID 66 | } 67 | 68 | this.updateCurrentIndex() 69 | return targetLayout 70 | } 71 | 72 | private updateCurrentIndex (): void { 73 | const idx = this.config.layoutOrder.indexOf(this.currentID) 74 | this.currentIndex = idx === -1 ? null : idx 75 | } 76 | 77 | private loadLayout (ID: string): WindowsLayout { 78 | let layout = this.layouts[ID] 79 | if (!layout) { 80 | layout = this.layouts[ID] = this.createLayoutFromId(ID) 81 | } 82 | return layout 83 | } 84 | 85 | private createLayoutFromId (id: string): WindowsLayout { 86 | if (id === MonocleLayout.id) { 87 | return new MonocleLayout(this.config) 88 | } else if (id === QuarterLayout.id) { 89 | return new QuarterLayout(this.config) 90 | } else if (id === SpiralLayout.id) { 91 | return new SpiralLayout(this.config) 92 | } else if (id === SpreadLayout.id) { 93 | return new SpreadLayout() 94 | } else if (id === StairLayout.id) { 95 | return new StairLayout() 96 | } else if (id === ThreeColumnLayout.id) { 97 | return new ThreeColumnLayout(this.config) 98 | } else if (id === TileLayout.id) { 99 | return new TileLayout(this.config) 100 | } else { 101 | return new FloatingLayout() 102 | } 103 | } 104 | } 105 | 106 | export default class LayoutStore { 107 | private store: Record 108 | 109 | constructor (private readonly config: Config) { 110 | this.store = {} 111 | } 112 | 113 | public getCurrentLayout (surface: IBridgeSurface): WindowsLayout { 114 | return surface.ignore 115 | ? FloatingLayout.instance 116 | : this.getEntry(String(surface.id)).currentLayout 117 | } 118 | 119 | public cycleLayout (surface: IBridgeSurface, step: 1 | -1): WindowsLayout | null { 120 | if (surface.ignore) { 121 | return null 122 | } 123 | return this.getEntry(String(surface.id)).cycleLayout(step) 124 | } 125 | 126 | public toggleLayout ( 127 | surface: IBridgeSurface, 128 | layoutClassID: string 129 | ): WindowsLayout | null { 130 | if (surface.ignore) { 131 | return null 132 | } 133 | return this.getEntry(String(surface.id)).toggleLayout(layoutClassID) 134 | } 135 | 136 | private getEntry (key: string): LayoutStoreEntry { 137 | if (!this.store[key]) { 138 | this.store[key] = new LayoutStoreEntry(this.config) 139 | } 140 | return this.store[key] 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/tile.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type WindowsLayout } from '.' 7 | import { 8 | RotateLayoutPart, 9 | HalfSplitLayoutPart, 10 | StackLayoutPart 11 | } from './part' 12 | 13 | import { WindowState, type IEngineWindow } from '../window' 14 | 15 | import { 16 | type Action, 17 | DecreaseLayoutMasterAreaSize, 18 | DecreaseMasterAreaWindowCount, 19 | IncreaseLayoutMasterAreaSize, 20 | IncreaseMasterAreaWindowCount, 21 | Rotate, 22 | RotateReverse, 23 | RotatePart 24 | } from '../../action' 25 | 26 | import { clip, slide } from '../../utils/func' 27 | import { type Rect, type RectDelta } from '../../utils/rect' 28 | 29 | import { type IWindowsManager } from '../../manager' 30 | import { type IEngine } from '..' 31 | import { type Config } from '@/config' 32 | 33 | export default class TileLayout implements WindowsLayout { 34 | public static readonly MIN_MASTER_RATIO = 0.2 35 | public static readonly MAX_MASTER_RATIO = 0.8 36 | public static readonly id = 'TileLayout' 37 | public readonly classID = TileLayout.id 38 | public readonly name = 'Tile Layout' 39 | public readonly icon = 'bismuth-tile' 40 | 41 | public get hint (): string { 42 | return String(this.numMaster) 43 | } 44 | 45 | private readonly parts: RotateLayoutPart< 46 | HalfSplitLayoutPart, StackLayoutPart> 47 | > 48 | 49 | private get numMaster (): number { 50 | return this.parts.inner.primarySize 51 | } 52 | 53 | private set numMaster (value: number) { 54 | this.parts.inner.primarySize = value 55 | } 56 | 57 | private get masterRatio (): number { 58 | return this.parts.inner.ratio 59 | } 60 | 61 | private set masterRatio (value: number) { 62 | this.parts.inner.ratio = value 63 | } 64 | 65 | private readonly config: Config 66 | 67 | constructor (config: Config) { 68 | this.config = config 69 | 70 | this.parts = new RotateLayoutPart( 71 | new HalfSplitLayoutPart( 72 | new RotateLayoutPart(new StackLayoutPart(this.config)), 73 | new StackLayoutPart(this.config) 74 | ) 75 | ) 76 | 77 | const masterPart = this.parts.inner 78 | masterPart.gap = 79 | masterPart.primary.inner.gap = 80 | masterPart.secondary.gap = 81 | this.config.gaps.tileLayout 82 | } 83 | 84 | public adjust ( 85 | area: Rect, 86 | tiles: IEngineWindow[], 87 | basis: IEngineWindow, 88 | delta: RectDelta 89 | ): void { 90 | this.parts.adjust(area, tiles, basis, delta) 91 | } 92 | 93 | public apply ( 94 | _controller: IWindowsManager, 95 | tileables: IEngineWindow[], 96 | area: Rect 97 | ): void { 98 | tileables.forEach((tileable) => (tileable.state = WindowState.Tiled)) 99 | 100 | this.parts.apply(area, tileables).forEach((geometry, i) => { 101 | tileables[i].geometry = geometry 102 | }) 103 | } 104 | 105 | public clone (): WindowsLayout { 106 | const other = new TileLayout(this.config) 107 | other.masterRatio = this.masterRatio 108 | other.numMaster = this.numMaster 109 | return other 110 | } 111 | 112 | public executeAction (engine: IEngine, action: Action): void { 113 | if (action instanceof DecreaseLayoutMasterAreaSize) { 114 | this.masterRatio = clip( 115 | slide(this.masterRatio, -0.05), 116 | TileLayout.MIN_MASTER_RATIO, 117 | TileLayout.MAX_MASTER_RATIO 118 | ) 119 | } else if (action instanceof IncreaseLayoutMasterAreaSize) { 120 | this.masterRatio = clip( 121 | slide(this.masterRatio, +0.05), 122 | TileLayout.MIN_MASTER_RATIO, 123 | TileLayout.MAX_MASTER_RATIO 124 | ) 125 | } else if (action instanceof IncreaseMasterAreaWindowCount) { 126 | // TODO: define arbitrary constant 127 | if (this.numMaster < 10) { 128 | this.numMaster += 1 129 | } 130 | engine.showLayoutNotification() 131 | } else if (action instanceof DecreaseMasterAreaWindowCount) { 132 | if (this.numMaster > 0) { 133 | this.numMaster -= 1 134 | } 135 | engine.showLayoutNotification() 136 | } else if (action instanceof Rotate) { 137 | this.parts.rotate(90) 138 | } else if (action instanceof RotateReverse) { 139 | this.parts.rotate(-90) 140 | } else if (action instanceof RotatePart) { 141 | this.parts.inner.primary.rotate(90) 142 | } else { 143 | action.executeWithoutLayoutOverride() 144 | } 145 | } 146 | 147 | public toString (): string { 148 | return `TileLayout(nmaster=${this.numMaster}, ratio=${this.masterRatio})` 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/utils/rect.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | export interface Frame { 7 | x: number 8 | y: number 9 | w: number 10 | h: number 11 | } 12 | 13 | export class Rect { 14 | constructor ( 15 | public x: number, 16 | public y: number, 17 | public width: number, 18 | public height: number 19 | ) {} 20 | 21 | public static fromFrame (value: Frame, mapfn?: (v: T) => Frame): Rect { 22 | const frame = mapfn ? mapfn(value) : value 23 | return new Rect(frame.x, frame.y, frame.w, frame.h) 24 | } 25 | 26 | public get maxX (): number { 27 | return this.x + this.width 28 | } 29 | 30 | public get maxY (): number { 31 | return this.y + this.height 32 | } 33 | 34 | public get center (): [number, number] { 35 | return [ 36 | this.x + Math.floor(this.width / 2), 37 | this.y + Math.floor(this.height / 2) 38 | ] 39 | } 40 | 41 | public clone (): Rect { 42 | return new Rect(this.x, this.y, this.width, this.height) 43 | } 44 | 45 | public equals (other: Rect): boolean { 46 | return ( 47 | this.x === other.x && 48 | this.y === other.y && 49 | this.width === other.width && 50 | this.height === other.height 51 | ) 52 | } 53 | 54 | public gap (left: number, right: number, top: number, bottom: number): Rect { 55 | return new Rect( 56 | this.x + left, 57 | this.y + top, 58 | this.width - (left + right), 59 | this.height - (top + bottom) 60 | ) 61 | } 62 | 63 | public gap_mut ( 64 | left: number, 65 | right: number, 66 | top: number, 67 | bottom: number 68 | ): this { 69 | this.x += left 70 | this.y += top 71 | this.width -= left + right 72 | this.height -= top + bottom 73 | return this 74 | } 75 | 76 | public includes (other: Rect): boolean { 77 | return ( 78 | this.x <= other.x && 79 | this.y <= other.y && 80 | other.maxX < this.maxX && 81 | other.maxY < this.maxY 82 | ) 83 | } 84 | 85 | public includesPoint ([x, y]: [number, number]): boolean { 86 | return this.x <= x && x <= this.maxX && this.y <= y && y <= this.maxY 87 | } 88 | 89 | public subtract (other: Rect): Rect { 90 | return new Rect( 91 | this.x - other.x, 92 | this.y - other.y, 93 | this.width - other.width, 94 | this.height - other.height 95 | ) 96 | } 97 | 98 | public toFrame (): Frame { 99 | const { x, y, width: w, height: h } = this 100 | 101 | return { x, y, w, h } 102 | } 103 | 104 | public distance (w1: Rect, w2: Rect): number { 105 | return Math.sqrt(Math.pow(w1.x - w2.x, 2) + Math.pow(w1.y - w2.y, 2)) 106 | } 107 | 108 | public overlapArea (w1: Rect, w2: Rect): number { 109 | const overlapX = Math.max(0, Math.min(w1.x + w1.width, w2.x + w2.width) - Math.max(w1.x, w2.x)) 110 | const overlapY = Math.max(0, Math.min(w1.y + w1.height, w2.y + w2.height) - Math.max(w1.y, w2.y)) 111 | return overlapX * overlapY 112 | } 113 | 114 | public isOverlapped (w1: Rect, w2: Rect): boolean { 115 | return (w1.x <= w2.x && w2.x <= w1.x + w1.width && w1.y <= w2.y && w2.y <= w1.y + w1.height) || 116 | (w2.x <= w1.x && w1.x <= w2.x + w2.width && w2.y <= w1.y && w1.y <= w2.y + w2.height) 117 | } 118 | 119 | public getOverlapFilter (draggedWindow: Rect): (window: Rect) => boolean { 120 | return (window: Rect) => this.isOverlapped(window, draggedWindow) 121 | } 122 | 123 | public getOverlapSorter (draggedWindow: Rect): (a: Rect, b: Rect) => number { 124 | return (a: Rect, b: Rect) => { 125 | const areaA = this.overlapArea(a, draggedWindow) 126 | const areaB = this.overlapArea(b, draggedWindow) 127 | 128 | return areaB - areaA 129 | } 130 | } 131 | 132 | public toString (): string { 133 | return 'Rect(' + [this.x, this.y, this.width, this.height].join(', ') + ')' 134 | } 135 | } 136 | 137 | /** 138 | * Describes geometric changes of a rectangle, in terms of changes per edge. 139 | * Outward changes are in positive, and inward changes are in negative. 140 | */ 141 | export class RectDelta { 142 | /** Generate a delta that transforms basis to target. */ 143 | public static fromRects (basis: Rect, target: Rect): RectDelta { 144 | const diff = target.subtract(basis) 145 | return new RectDelta( 146 | diff.width + diff.x, 147 | -diff.x, 148 | diff.height + diff.y, 149 | -diff.y 150 | ) 151 | } 152 | 153 | constructor ( 154 | public readonly east: number, 155 | public readonly west: number, 156 | public readonly south: number, 157 | public readonly north: number 158 | ) {} 159 | 160 | public toString (): string { 161 | return `WindowResizeDelta(east=${this.east} west=${this.west} north=${this.north} south=${this.south}}` 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /apps/yakite-daemon/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # yakite-daemon 2 | 3 | ## 0.1.10 4 | 5 | ### Patch Changes 6 | 7 | - [`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - fix: remove the duplicates FloatingLayout 8 | 9 | - Updated dependencies [[`116da02`](https://github.com/I-Want-ToBelieve/yakite/commit/116da022e5a20e03256efac8d111d8632ee83aff)]: 10 | - krohnkite-core@0.1.10 11 | - yakite-bridge@0.1.10 12 | - yakite-config@0.1.10 13 | - yakite-message@0.1.10 14 | - yakite-yabai@0.1.10 15 | 16 | ## 0.1.9 17 | 18 | ### Patch Changes 19 | 20 | - [`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore(yakite-toast): add arm support 21 | 22 | - Updated dependencies [[`c2b840d`](https://github.com/I-Want-ToBelieve/yakite/commit/c2b840df4141c3ef7c16c69c4e06dd1e2c7c525b)]: 23 | - krohnkite-core@0.1.9 24 | - yakite-bridge@0.1.9 25 | - yakite-config@0.1.9 26 | - yakite-message@0.1.9 27 | - yakite-yabai@0.1.9 28 | 29 | ## 0.1.8 30 | 31 | ### Patch Changes 32 | 33 | - [`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: enable file log 34 | 35 | - Updated dependencies [[`a4d170a`](https://github.com/I-Want-ToBelieve/yakite/commit/a4d170a88058be0fd7b38666fec43d8f4ee017ba)]: 36 | - krohnkite-core@0.1.8 37 | - yakite-bridge@0.1.8 38 | - yakite-config@0.1.8 39 | - yakite-message@0.1.8 40 | - yakite-yabai@0.1.8 41 | 42 | ## 0.1.7 43 | 44 | ### Patch Changes 45 | 46 | - [`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - feat: fuck github action 47 | 48 | - Updated dependencies [[`3614d77`](https://github.com/I-Want-ToBelieve/yakite/commit/3614d77c91d342ab7560ff8e7508f01bc8954019)]: 49 | - krohnkite-core@0.1.7 50 | - yakite-bridge@0.1.7 51 | - yakite-config@0.1.7 52 | - yakite-message@0.1.7 53 | - yakite-yabai@0.1.7 54 | 55 | ## 0.1.6 56 | 57 | ### Patch Changes 58 | 59 | - [`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: use deploy key instead of a PAT. 60 | 61 | - Updated dependencies [[`5d81753`](https://github.com/I-Want-ToBelieve/yakite/commit/5d817533c827ec2ecdada77784cdc11f062cd41f)]: 62 | - krohnkite-core@0.1.6 63 | - yakite-bridge@0.1.6 64 | - yakite-config@0.1.6 65 | - yakite-message@0.1.6 66 | - yakite-yabai@0.1.6 67 | 68 | ## 0.1.5 69 | 70 | ### Patch Changes 71 | 72 | - [`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: try to trigger github action tags monitoring 73 | 74 | - Updated dependencies [[`50c083e`](https://github.com/I-Want-ToBelieve/yakite/commit/50c083e0c9fed1806c1db146d57accc2ac9ea1c8)]: 75 | - krohnkite-core@0.1.5 76 | - yakite-bridge@0.1.5 77 | - yakite-config@0.1.5 78 | - yakite-message@0.1.5 79 | - yakite-yabai@0.1.5 80 | 81 | ## 0.1.4 82 | 83 | ### Patch Changes 84 | 85 | - [`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: hit github action more accurately 86 | 87 | - Updated dependencies [[`12b13f7`](https://github.com/I-Want-ToBelieve/yakite/commit/12b13f7ab413d413a2f12723598c3e49e733e910)]: 88 | - krohnkite-core@0.1.4 89 | - yakite-bridge@0.1.4 90 | - yakite-config@0.1.4 91 | - yakite-message@0.1.4 92 | - yakite-yabai@0.1.4 93 | 94 | ## 0.1.3 95 | 96 | ### Patch Changes 97 | 98 | - [`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b) Thanks [@I-Want-ToBelieve](https://github.com/I-Want-ToBelieve)! - chore: release crates and use @changesets/changelog-github 99 | 100 | - Updated dependencies [[`2098cc9`](https://github.com/I-Want-ToBelieve/yakite/commit/2098cc9f46d150498a8327d344dd7811748d5a8b)]: 101 | - krohnkite-core@0.1.3 102 | - yakite-bridge@0.1.3 103 | - yakite-config@0.1.3 104 | - yakite-message@0.1.3 105 | - yakite-yabai@0.1.3 106 | 107 | ## 0.1.2 108 | 109 | ### Patch Changes 110 | 111 | - 1e7cc3b: chore: update zeromq from 6.0.0-beta.17 to 6.0.0-beta.19 112 | - Updated dependencies [1e7cc3b] 113 | - krohnkite-core@0.1.2 114 | - yakite-bridge@0.1.2 115 | - yakite-config@0.1.2 116 | - yakite-message@0.1.2 117 | - yakite-yabai@0.1.2 118 | 119 | ## 0.1.1 120 | 121 | ### Patch Changes 122 | 123 | - 1c80812: feat: first release 124 | - Updated dependencies [1c80812] 125 | - krohnkite-core@0.1.1 126 | - yakite-message@0.1.1 127 | - yakite-bridge@0.1.1 128 | - yakite-config@0.1.1 129 | - yakite-yabai@0.1.1 130 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/store/window.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type IEngineWindow } from '../window' 7 | 8 | import { type IBridgeSurface } from '../../bridge/surface' 9 | 10 | /** 11 | * Window storage facility with convenient window filters built-in. 12 | */ 13 | export interface IWindowStore { 14 | /** 15 | * Returns all visible windows on the given surface. 16 | */ 17 | visibleWindowsOn: (surf: IBridgeSurface) => IEngineWindow[] 18 | 19 | /** 20 | * Return all visible "Tile" windows on the given surface. 21 | */ 22 | visibleTiledWindowsOn: (surf: IBridgeSurface) => IEngineWindow[] 23 | 24 | /** 25 | * Return all visible "tileable" windows on the given surface 26 | * @see Window#tileable 27 | */ 28 | visibleTileableWindowsOn: (surf: IBridgeSurface) => IEngineWindow[] 29 | 30 | /** 31 | * Return all "tileable" windows on the given surface, including hidden 32 | */ 33 | tileableWindowsOn: (surf: IBridgeSurface) => IEngineWindow[] 34 | 35 | /** 36 | * Return all windows on this surface, including minimized windows 37 | */ 38 | allWindowsOn: (surf: IBridgeSurface) => IEngineWindow[] 39 | 40 | /** 41 | * Inserts the window at the beginning 42 | */ 43 | unshift: (window: IEngineWindow) => void 44 | 45 | /** 46 | * Inserts the window at the end 47 | */ 48 | push: (window: IEngineWindow) => void 49 | 50 | /** 51 | * Remove window from the store 52 | */ 53 | remove: (window: IEngineWindow) => void 54 | 55 | /** 56 | * Move srcWin to the destWin position (before/after) 57 | * @param after if true, srcWin is moved after the destWindow. If false - it is moved before. 58 | */ 59 | move: (srcWin: IEngineWindow, destWin: IEngineWindow, after?: boolean) => void 60 | 61 | /** 62 | * Swap windows positions 63 | */ 64 | swap: (alpha: IEngineWindow, beta: IEngineWindow) => void 65 | 66 | /** 67 | * Put the window into the master area. 68 | * @param window window to put into the master area 69 | */ 70 | putWindowToMaster: (window: IEngineWindow) => void 71 | } 72 | 73 | export class WindowStore implements IWindowStore { 74 | /** 75 | * @param list window list to initialize from 76 | */ 77 | constructor (public list: IEngineWindow[] = []) {} 78 | 79 | public move ( 80 | srcWin: IEngineWindow, 81 | destWin: IEngineWindow, 82 | after?: boolean 83 | ): void { 84 | const srcIdx = this.list.indexOf(srcWin) 85 | const destIdx = this.list.indexOf(destWin) 86 | if (srcIdx === -1 || destIdx === -1) { 87 | return 88 | } 89 | 90 | // Delete the source window 91 | this.list.splice(srcIdx, 1) 92 | // Place the source window in before destination window or after it 93 | this.list.splice(after ? destIdx + 1 : destIdx, 0, srcWin) 94 | } 95 | 96 | public putWindowToMaster (window: IEngineWindow): void { 97 | const idx = this.list.indexOf(window) 98 | if (idx === -1) { 99 | return 100 | } 101 | this.list.splice(idx, 1) 102 | this.list.splice(0, 0, window) 103 | } 104 | 105 | public swap (alpha: IEngineWindow, beta: IEngineWindow): void { 106 | const alphaIndex = this.list.indexOf(alpha) 107 | const betaIndex = this.list.indexOf(beta) 108 | if (alphaIndex < 0 || betaIndex < 0) { 109 | return 110 | } 111 | 112 | this.list[alphaIndex] = beta 113 | this.list[betaIndex] = alpha 114 | } 115 | 116 | public get length (): number { 117 | return this.list.length 118 | } 119 | 120 | public at (idx: number): IEngineWindow { 121 | return this.list[idx] 122 | } 123 | 124 | public indexOf (window: IEngineWindow): number { 125 | return this.list.indexOf(window) 126 | } 127 | 128 | public push (window: IEngineWindow): void { 129 | this.list.push(window) 130 | } 131 | 132 | public remove (window: IEngineWindow): void { 133 | const idx = this.list.indexOf(window) 134 | if (idx >= 0) { 135 | this.list.splice(idx, 1) 136 | } 137 | } 138 | 139 | public unshift (window: IEngineWindow): void { 140 | this.list.unshift(window) 141 | } 142 | 143 | public visibleWindowsOn (surf: IBridgeSurface): IEngineWindow[] { 144 | return this.list.filter((win) => win.visibleOn(surf)) 145 | } 146 | 147 | public visibleTiledWindowsOn (surf: IBridgeSurface): IEngineWindow[] { 148 | return this.list.filter((win) => win.tiled && win.visibleOn(surf)) 149 | } 150 | 151 | public visibleTileableWindowsOn (surf: IBridgeSurface): IEngineWindow[] { 152 | return this.list.filter((win) => win.tileable && win.visibleOn(surf)) 153 | } 154 | 155 | public tileableWindowsOn (surf: IBridgeSurface): IEngineWindow[] { 156 | return this.list.filter( 157 | (win) => win.tileable && win.surface.id === surf.id 158 | ) 159 | } 160 | 161 | public allWindowsOn (surf: IBridgeSurface): IEngineWindow[] { 162 | return this.list.filter((win) => win.surface.id === surf.id) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /apps/yakite-bridge/src/window.ts: -------------------------------------------------------------------------------- 1 | import { Rect, matchWords } from 'krohnkite-core' 2 | import { YakiteBridgeSurface } from './surface' 3 | import { type Config, type IBridgeWindow, type IBridgeSurface } from 'krohnkite-core' 4 | import { type YabaiSpace, type YabaiDisplay, type YabaiWindow } from 'yakite-yabai' 5 | import type Yabai from 'yakite-yabai' 6 | 7 | export class YakiteBridgeWindow implements IBridgeWindow { 8 | /** 9 | * Create a window from the Yabai Window object 10 | * 11 | * @param id 12 | * @param config 13 | * @param log 14 | */ 15 | constructor ( 16 | public readonly id: YabaiWindow['id'], 17 | private readonly config: Config, 18 | private readonly yabai: Yabai 19 | ) { 20 | this.maximized = this.fullScreen 21 | } 22 | 23 | private savedWindow: YabaiWindow | null = null 24 | 25 | public get window (): YabaiWindow { 26 | const window = this.yabai.windows.find((it) => +it.id === +this.id) 27 | 28 | if (window) { 29 | this.savedWindow = window 30 | } 31 | 32 | return this.savedWindow as YabaiWindow 33 | } 34 | 35 | public get fullScreen (): boolean { 36 | return this.window['is-native-fullscreen'] 37 | } 38 | 39 | public get geometry (): Rect { 40 | return Rect.fromFrame(this.window.frame) 41 | } 42 | 43 | public get active (): boolean { 44 | return this.window['has-focus'] 45 | } 46 | 47 | public get shouldIgnore (): boolean { 48 | const { app, role, title } = this.window 49 | 50 | return ( 51 | this.window['is-hidden'] || 52 | this.config.ignore.class.includes(app) || 53 | matchWords(title, this.config.ignore.title) >= 0 || 54 | this.config.ignore.role.includes(role) 55 | ) 56 | } 57 | 58 | public get shouldFloat (): boolean { 59 | const { app, title } = this.window 60 | 61 | return ( 62 | !this.window['can-resize'] || 63 | this.isDialog || 64 | this.config.floating.class.includes(app) || 65 | matchWords(title, this.config.floating.title) >= 0 66 | ) 67 | } 68 | 69 | public get screen (): number { 70 | return this.window.display 71 | } 72 | 73 | public get minimized (): boolean { 74 | return this.window['is-minimized'] 75 | } 76 | 77 | public set minimized (min: boolean) { 78 | if (min) { 79 | void this.yabai.minimize(this.id) 80 | } else { 81 | void this.yabai.deminimize(this.id) 82 | } 83 | } 84 | 85 | public readonly shaded = false 86 | 87 | public maximized: boolean 88 | 89 | public get surface (): IBridgeSurface { 90 | const { displays, spaces } = this.yabai 91 | 92 | return new YakiteBridgeSurface( 93 | (displays.find((it) => it.index === this.window.display) as YabaiDisplay).id, 94 | (spaces.find((it) => it.index === this.window.space) as YabaiSpace).id 95 | , 96 | this.config, 97 | this.yabai 98 | ) 99 | } 100 | 101 | public set surface (surface: YakiteBridgeSurface) { 102 | // TODO: setting activity? 103 | // TODO: setting screen = move to the screen 104 | if (this.window.space !== surface.space.index) { 105 | this.window.space = surface.space.index 106 | } 107 | } 108 | 109 | public static generateID (window: YabaiWindow): string { 110 | return `${window.app}-${window.id}` 111 | } 112 | 113 | public async commit ( 114 | geometry?: Rect, 115 | _noBorder?: boolean, 116 | keepAbove?: boolean 117 | ): Promise { 118 | // const ID = YakiteBridgeWindow.generateID(this.window) 119 | // console.log(this.yabai.cachedFrame[ID]) 120 | // if (this.yabai.cachedFrame[ID]?.moveing) { 121 | // return 122 | // } 123 | 124 | // if (this.yabai.cachedFrame[ID]?.resizeing) { 125 | // return 126 | // } 127 | 128 | if (keepAbove) { 129 | await this.yabai.above(this.window.id) 130 | } else if (keepAbove === false) { 131 | await this.yabai.normal(this.window.id) 132 | } 133 | 134 | if (geometry !== undefined) { 135 | const frame = this.adjustGeometry(geometry).toFrame() 136 | const { x, y, w, h } = frame 137 | await this.yabai.move({ x, y }, this.id) 138 | await this.yabai.resize({ w, h }, this.id) 139 | } 140 | } 141 | 142 | public toString (): string { 143 | // Using a shorthand name to keep debug message tidy 144 | return `Win(${this.window.id.toString()}.${ 145 | this.window.app + this.window.title 146 | })` 147 | } 148 | 149 | public visibleOn (surface: IBridgeSurface): boolean { 150 | const yakiteSurface = surface as YakiteBridgeSurface 151 | 152 | return ( 153 | !this.minimized && 154 | (this.window.space === yakiteSurface.space.index || this.window['is-sticky']) && /* on all desktop */ 155 | this.window.display === yakiteSurface.display.index 156 | ) 157 | } 158 | 159 | /** 160 | * Apply various resize hints to the given geometry 161 | * @param geometry 162 | * @returns 163 | */ 164 | private adjustGeometry (geometry: Rect): Rect { 165 | let width = geometry.width 166 | let height = geometry.height 167 | 168 | /* do not resize fixed-size windows */ 169 | if (!this.window['can-resize']) { 170 | width = this.window.frame.w 171 | height = this.window.frame.h 172 | } 173 | 174 | return new Rect(geometry.x, geometry.y, width, height) 175 | } 176 | 177 | public get isDialog (): boolean { 178 | // TODO 179 | return this.window.subrole === 'AXDialog' 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/quarter.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type WindowsLayout } from '.' 7 | 8 | import { WindowState, type IEngineWindow } from '../window' 9 | 10 | import { clip } from '../../utils/func' 11 | import { Rect, type RectDelta } from '../../utils/rect' 12 | 13 | import { type IWindowsManager } from '../../manager' 14 | import { type Config } from '@/config' 15 | 16 | export default class QuarterLayout implements WindowsLayout { 17 | public static readonly MAX_PROPORTION = 0.8 18 | public static readonly id = 'QuarterLayout' 19 | public readonly classID = QuarterLayout.id 20 | public readonly name = 'Quarter Layout' 21 | public readonly icon = 'bismuth-quarter' 22 | 23 | public readonly capacity = 4 24 | 25 | private lhsplit: number 26 | private rhsplit: number 27 | private vsplit: number 28 | 29 | private readonly config: Config 30 | 31 | public constructor (config: Config) { 32 | this.lhsplit = 0.5 33 | this.rhsplit = 0.5 34 | this.vsplit = 0.5 35 | 36 | this.config = config 37 | } 38 | 39 | public adjust ( 40 | area: Rect, 41 | tiles: IEngineWindow[], 42 | basis: IEngineWindow, 43 | delta: RectDelta 44 | ): void { 45 | if (tiles.length <= 1 || tiles.length > 4) { 46 | return 47 | } 48 | 49 | const idx = tiles.indexOf(basis) 50 | if (idx < 0) { 51 | return 52 | } 53 | 54 | /* vertical split */ 55 | if ((idx === 0 || idx === 3) && delta.east !== 0) { 56 | this.vsplit = 57 | (Math.floor(area.width * this.vsplit) + delta.east) / area.width 58 | } else if ((idx === 1 || idx === 2) && delta.west !== 0) { 59 | this.vsplit = 60 | (Math.floor(area.width * this.vsplit) - delta.west) / area.width 61 | } 62 | 63 | /* left-side horizontal split */ 64 | if (tiles.length === 4) { 65 | if (idx === 0 && delta.south !== 0) { 66 | this.lhsplit = 67 | (Math.floor(area.height * this.lhsplit) + delta.south) / area.height 68 | } 69 | if (idx === 3 && delta.north !== 0) { 70 | this.lhsplit = 71 | (Math.floor(area.height * this.lhsplit) - delta.north) / area.height 72 | } 73 | } 74 | 75 | /* right-side horizontal split */ 76 | if (tiles.length >= 3) { 77 | if (idx === 1 && delta.south !== 0) { 78 | this.rhsplit = 79 | (Math.floor(area.height * this.rhsplit) + delta.south) / area.height 80 | } 81 | if (idx === 2 && delta.north !== 0) { 82 | this.rhsplit = 83 | (Math.floor(area.height * this.rhsplit) - delta.north) / area.height 84 | } 85 | } 86 | 87 | /* clipping */ 88 | this.vsplit = clip( 89 | this.vsplit, 90 | 1 - QuarterLayout.MAX_PROPORTION, 91 | QuarterLayout.MAX_PROPORTION 92 | ) 93 | this.lhsplit = clip( 94 | this.lhsplit, 95 | 1 - QuarterLayout.MAX_PROPORTION, 96 | QuarterLayout.MAX_PROPORTION 97 | ) 98 | this.rhsplit = clip( 99 | this.rhsplit, 100 | 1 - QuarterLayout.MAX_PROPORTION, 101 | QuarterLayout.MAX_PROPORTION 102 | ) 103 | } 104 | 105 | public clone (): WindowsLayout { 106 | const other = new QuarterLayout(this.config) 107 | other.lhsplit = this.lhsplit 108 | other.rhsplit = this.rhsplit 109 | other.vsplit = this.vsplit 110 | return other 111 | } 112 | 113 | public apply ( 114 | _controller: IWindowsManager, 115 | tileables: IEngineWindow[], 116 | area: Rect 117 | ): void { 118 | for (let i = 0; i < 4 && i < tileables.length; i++) { 119 | tileables[i].state = WindowState.Tiled 120 | } 121 | 122 | if (tileables.length > 4) { 123 | tileables 124 | .slice(4) 125 | .forEach((tile) => (tile.state = WindowState.TiledAfloat)) 126 | } 127 | 128 | if (tileables.length === 1) { 129 | tileables[0].geometry = area 130 | return 131 | } 132 | 133 | const gap1 = Math.floor(this.config.gaps.tileLayout / 2) 134 | const gap2 = this.config.gaps.tileLayout - gap1 135 | 136 | const leftWidth = Math.floor(area.width * this.vsplit) 137 | const rightWidth = area.width - leftWidth 138 | const rightX = area.x + leftWidth 139 | if (tileables.length === 2) { 140 | tileables[0].geometry = new Rect( 141 | area.x, 142 | area.y, 143 | leftWidth, 144 | area.height 145 | ).gap(0, gap1, 0, 0) 146 | tileables[1].geometry = new Rect( 147 | rightX, 148 | area.y, 149 | rightWidth, 150 | area.height 151 | ).gap(gap2, 0, 0, 0) 152 | return 153 | } 154 | 155 | const rightTopHeight = Math.floor(area.height * this.rhsplit) 156 | const rightBottomHeight = area.height - rightTopHeight 157 | const rightBottomY = area.y + rightTopHeight 158 | if (tileables.length === 3) { 159 | tileables[0].geometry = new Rect( 160 | area.x, 161 | area.y, 162 | leftWidth, 163 | area.height 164 | ).gap(0, gap1, 0, 0) 165 | tileables[1].geometry = new Rect( 166 | rightX, 167 | area.y, 168 | rightWidth, 169 | rightTopHeight 170 | ).gap(gap2, 0, 0, gap1) 171 | tileables[2].geometry = new Rect( 172 | rightX, 173 | rightBottomY, 174 | rightWidth, 175 | rightBottomHeight 176 | ).gap(gap2, 0, gap2, 0) 177 | return 178 | } 179 | 180 | const leftTopHeight = Math.floor(area.height * this.lhsplit) 181 | const leftBottomHeight = area.height - leftTopHeight 182 | const leftBottomY = area.y + leftTopHeight 183 | if (tileables.length >= 4) { 184 | tileables[0].geometry = new Rect( 185 | area.x, 186 | area.y, 187 | leftWidth, 188 | leftTopHeight 189 | ).gap(0, gap1, 0, gap1) 190 | tileables[1].geometry = new Rect( 191 | rightX, 192 | area.y, 193 | rightWidth, 194 | rightTopHeight 195 | ).gap(gap2, 0, 0, gap1) 196 | tileables[2].geometry = new Rect( 197 | rightX, 198 | rightBottomY, 199 | rightWidth, 200 | rightBottomHeight 201 | ).gap(gap2, 0, gap2, 0) 202 | tileables[3].geometry = new Rect( 203 | area.x, 204 | leftBottomY, 205 | leftWidth, 206 | leftBottomHeight 207 | ).gap(0, gap2, gap2, 0) 208 | } 209 | } 210 | 211 | public toString (): string { 212 | return 'QuarterLayout()' 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /apps/yakite-toast/src/yakite-toast.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface ToastWindow : NSPanel 6 | @end 7 | 8 | @implementation ToastWindow 9 | 10 | - (BOOL)canBecomeKeyWindow { 11 | return YES; 12 | } 13 | 14 | - (BOOL)canBecomeMainWindow { 15 | return YES; 16 | } 17 | 18 | - (void)mouseDown:(NSEvent *)theEvent { 19 | // Do nothing 20 | } 21 | 22 | @end 23 | 24 | void handleSIGTERM(int signum) { 25 | // Re-raise the signal to terminate the program. 26 | signal(signum, SIG_DFL); 27 | raise(signum); 28 | } 29 | 30 | int main(int argc, const char * argv[]) { 31 | @autoreleasepool { 32 | NSString *path = @"/tmp/yakite-toast.pid"; 33 | NSFileManager *fileManager = [NSFileManager defaultManager]; 34 | 35 | signal(SIGTERM, handleSIGTERM); 36 | 37 | if ([fileManager fileExistsAtPath:path]) { 38 | NSString *pidString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; 39 | pid_t pid = [pidString intValue]; 40 | kill(pid, SIGTERM); 41 | } 42 | 43 | pid_t myPid = getpid(); 44 | NSString *myPidString = [NSString stringWithFormat:@"%d", myPid]; 45 | [myPidString writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil]; 46 | 47 | NSApplication *app = [NSApplication sharedApplication]; 48 | 49 | // Parse command line arguments 50 | NSArray *arguments = [[NSProcessInfo processInfo] arguments]; 51 | BOOL version = [arguments containsObject:@"--version"]; 52 | BOOL help = [arguments containsObject:@"--help"]; 53 | 54 | if (version) { 55 | printf("Version: 0.1.10\n"); 56 | return 0; 57 | } 58 | 59 | if (help) { 60 | printf("Usage: %s [--version] [--help] [--text] [--time] [--position]\n", 61 | argv[0]); 62 | printf("Options:\n"); 63 | printf(" --version Print the version number\n"); 64 | printf(" --help Show this help message\n"); 65 | printf(" --text Set the text (default: \"Default text\")\n"); 66 | printf(" --time Set the display time (default: 1.0)\n"); 67 | printf(" --position Set the position (default: \"center\")\n"); 68 | return 0; 69 | } 70 | 71 | // Parse command line arguments 72 | NSDictionary *args = 73 | [[NSUserDefaults standardUserDefaults] dictionaryRepresentation]; 74 | 75 | NSString *text = args[@"-text"] ?: @"Hello World!"; 76 | NSTimeInterval time = [args[@"-time"] doubleValue] ?: 1.5; 77 | NSString *position = args[@"-position"] ?: @"center"; 78 | 79 | // Create window 80 | 81 | ToastWindow *window = [[ToastWindow alloc] 82 | initWithContentRect:NSMakeRect(0, 0, 380, 100) 83 | styleMask:NSWindowStyleMaskNonactivatingPanel | 84 | NSWindowStyleMaskFullSizeContentView 85 | backing:NSBackingStoreBuffered 86 | defer:NO]; 87 | 88 | [window setOpaque:NO]; 89 | [window setHasShadow:NO]; 90 | [window setMovableByWindowBackground:YES]; 91 | [window setStyleMask:NSWindowStyleMaskFullSizeContentView]; 92 | [window.contentView setWantsLayer:YES]; 93 | window.contentView.layer.backgroundColor = 94 | [[NSColor colorWithWhite:1 alpha:0.5] CGColor]; 95 | window.contentView.layer.cornerRadius = 10; 96 | window.contentView.layer.masksToBounds = YES; 97 | 98 | [window setLevel:NSFloatingWindowLevel]; 99 | [window setBackgroundColor:[NSColor colorWithWhite:1 alpha:0]]; 100 | [window setIgnoresMouseEvents:YES]; 101 | 102 | // Set window position 103 | NSRect screenRect = [[NSScreen mainScreen] frame]; 104 | NSRect windowRect = [window frame]; 105 | if ([position isEqualToString:@"top"]) { 106 | windowRect.origin.y = screenRect.size.height - windowRect.size.height; 107 | } else if ([position isEqualToString:@"bottom"]) { 108 | windowRect.origin.y = 0; 109 | } else if ([position isEqualToString:@"left"]) { 110 | windowRect.origin.x = 0; 111 | } else if ([position isEqualToString:@"right"]) { 112 | windowRect.origin.x = screenRect.size.width - windowRect.size.width; 113 | } else if ([position isEqualToString:@"center"]) { 114 | windowRect.origin.x = (screenRect.size.width - windowRect.size.width) / 2; 115 | windowRect.origin.y = 116 | (screenRect.size.height - windowRect.size.height) / 2; 117 | } 118 | [window setFrame:windowRect display:YES]; 119 | 120 | // Create visual effect view 121 | NSVisualEffectView *visualEffectView = [[NSVisualEffectView alloc] 122 | initWithFrame:[[window contentView] bounds]]; 123 | [visualEffectView 124 | setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; 125 | [visualEffectView setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; 126 | [visualEffectView setMaterial:NSVisualEffectMaterialUnderWindowBackground]; 127 | [visualEffectView setState:NSVisualEffectStateActive]; 128 | 129 | // Create text field 130 | NSTextView *textView = 131 | [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, 380, 35)]; 132 | [textView setString:text]; 133 | [textView setFont:[NSFont boldSystemFontOfSize:35]]; 134 | 135 | [textView setEditable:NO]; 136 | [textView setSelectable:NO]; 137 | [textView setBackgroundColor:[NSColor clearColor]]; 138 | [textView setDrawsBackground:NO]; 139 | [textView setVerticallyResizable:YES]; 140 | [textView setAlignment:NSTextAlignmentCenter]; 141 | [textView 142 | setTextContainerInset:NSMakeSize(0, ([window frame].size.height - 143 | [textView frame].size.height) / 144 | 2)]; 145 | 146 | // Add subviews 147 | [visualEffectView addSubview:textView]; 148 | [[window contentView] addSubview:visualEffectView]; 149 | 150 | // Show window 151 | [window makeKeyAndOrderFront:app]; 152 | 153 | // Hide window after delay 154 | dispatch_after( 155 | dispatch_time(DISPATCH_TIME_NOW, (int64_t)(time * NSEC_PER_SEC)), 156 | dispatch_get_main_queue(), ^{ 157 | [window orderOut:app]; 158 | [NSApp terminate:app]; 159 | }); 160 | 161 | // Run app 162 | [app run]; 163 | } 164 | return 0; 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Yakite](https://github.com/I-Want-ToBelieve/yakite) 2 | --- 3 | 4 | A dynamic tiled window management that bridges the gap between [yabai][] and [krohnkite][] 5 | 6 | [![Watch the video](https://github.com/koekeishiya/yabai/assets/24669431/615044d8-cdee-479f-a1ba-bc35af9b0118)](https://github.com/koekeishiya/yabai/assets/24669431/615044d8-cdee-479f-a1ba-bc35af9b0118) 7 | 8 | 9 | 10 | ## Prerequisites 11 | 12 | Necessary: 13 | - [yabai][] 14 | 15 | ## Install 16 | 17 | brew 18 | ```fish 19 | brew tap I-Want-ToBelieve/homebrew-formulae 20 | 21 | brew install node yakite yakite-toast 22 | 23 | # or brew install yakite-toast-bin 24 | 25 | npm --global install yakite-daemon 26 | ``` 27 | 28 | The layout is completed by [krohnkite-core][], which is already embedded in yakite-daemon, so you only need to install yakite-daemon. 29 | 30 | Although the name [krohnkite-core][] is used, its source code is obtained from [bismuth][], not [krohnkite][], which is a fork of [krohnkite][] 31 | 32 | The yakite-toast is a message window that displays the current layout when the layout is switched. 33 | 34 | The yakite-daemon is written in typescript and therefore relies on the node runtime. 35 | 36 | The yakite is the messenger for communication between yakite-daemon and [yabai][] and skhd processes. 37 | 38 | Or nix 39 | ```nix 40 | {pkgs, ...}: 41 | { 42 | homebrew = { 43 | enable = true; 44 | 45 | onActivation = { 46 | autoUpdate = false; 47 | # 'zap': uninstalls all formulae(and related files) not listed here. 48 | cleanup = "zap"; 49 | }; 50 | 51 | # Applications to install from Mac App Store using mas. 52 | # You need to install all these Apps manually first so that your apple account have records for them. 53 | # otherwise Apple Store will refuse to install them. 54 | # For details, see https://githubfast.com/mas-cli/mas 55 | masApps = { 56 | # TODO Feel free to add your favorite apps here. 57 | 58 | # Xcode = 497799835; 59 | }; 60 | 61 | taps = [ 62 | "homebrew/cask" 63 | "homebrew/cask-fonts" 64 | "homebrew/services" 65 | "homebrew/cask-versions" 66 | "FelixKratz/formulae" 67 | "I-Want-ToBelieve/homebrew-formulae" 68 | ]; 69 | 70 | # `brew install` 71 | # TODO Feel free to add your favorite apps here. 72 | brews = [ 73 | "node" 74 | "yakite" 75 | "yakite-toast-bin" 76 | ]; 77 | 78 | # `brew install --cask` 79 | # TODO Feel free to add your favorite apps here. 80 | casks = [ 81 | ]; 82 | }; 83 | } 84 | ``` 85 | 86 | 87 | ## Configuration 88 | 89 | ### yakite.json 90 | The configuration of yakite itself is in `~/.config/yakite/yakite.json` 91 | It will automatically generate this configuration file with default configuration on first run. 92 | 93 | It is worth mentioning that the `class` field in `yakite.json` is actually the `app` field in `yabai -m query --windows` 94 | 95 | 96 | ### yabairc 97 | 98 | ```zsh 99 | yabai -m config focus_follows_mouse off 100 | yabai -m config mouse_follows_focus off 101 | yabai -m config window_opacity 0.900000 102 | 103 | borders active_color=0xffe1e3e4 inactive_color=0xff494d64 width=5.0 2>/dev/null 1>&2 & 104 | 105 | yakite-daemon 2>/dev/null 1>&2 & 106 | ``` 107 | 108 | ### skhdrc 109 | ```zsh 110 | ctrl - return : kitty --single-instance -d ~ 111 | 112 | cmd - backspace : skhd -k "ctrl - space" 113 | 114 | ctrl - w : skhd -k "ctrl - up" 115 | 116 | ctrl - 1 : yabai -m space --focus 1 117 | ctrl - 2 : yabai -m space --focus 2 118 | ctrl - 3 : yabai -m space --focus 3 119 | ctrl - 4 : yabai -m space --focus 4 120 | ctrl - 5 : yabai -m space --focus 5 121 | ctrl - 6 : yabai -m space --focus 6 122 | ctrl - 7 : yabai -m space --focus 7 123 | ctrl - 8 : yabai -m space --focus 8 124 | ctrl - 9 : yabai -m space --focus 9 125 | ctrl - 0 : yabai -m space --focus 10 126 | 127 | ctrl + shift - j : yabai -m window --space prev 128 | ctrl + shift - l : yabai -m window --space next 129 | ctrl + shift - 1 : yabai -m window --space 1 130 | ctrl + shift - 2 : yabai -m window --space 2 131 | ctrl + shift - 3 : yabai -m window --space 3 132 | ctrl + shift - 4 : yabai -m window --space 4 133 | ctrl + shift - 5 : yabai -m window --space 5 134 | ctrl + shift - 6 : yabai -m window --space 6 135 | ctrl + shift - 7 : yabai -m window --space 7 136 | ctrl + shift - 8 : yabai -m window --space 8 137 | ctrl + shift - 9 : yabai -m window --space 9 138 | ctrl + shift - 0 : yabai -m window --space 10 139 | 140 | 141 | ctrl - q : yabai -m window --close 142 | ctrl - x : yabai -m window --minimize 143 | 144 | ctrl - t : yakite action toggle-tile-layout 145 | ctrl - m : yakite action toggle-monocle-layout 146 | ctrl - f : yakite action toggle-active-window-floating 147 | ctrl - k : yakite action focus-next-window 148 | ctrl - i : yakite action focus-previous-window 149 | ctrl + shift - k : yakite action move-active-window-to-next-position 150 | ctrl + shift - i : yakite action move-active-window-to-previous-position 151 | ctrl + shift - m : yakite action push-active-window-into-master-area-front 152 | 153 | ctrl - 0x2A : yakite action switch-to-next-layout 154 | 155 | ctrl - j : yakite action decrease-layout-master-area-size 156 | ctrl - l : yakite action increase-layout-master-area-size 157 | 158 | ``` 159 | 160 | 0x2A is `\` 161 | 162 | 163 | ## DEV VERSION 164 | 165 | ### Prerequisites 166 | 167 | Necessary: 168 | - [yabai][] 169 | - [nix-install-macos][] 170 | - [nix-direnv][] 171 | 172 | Optional: 173 | - [nix-darwin][] 174 | 175 | 176 | ### Compile and Run 177 | 178 | In your terminal: 179 | ```fish 180 | mkdir ~/git.workspace 181 | cd ~/git.workspace 182 | git clone https://github.com/I-Want-ToBelieve/yakite.git 183 | cd yakite 184 | direnv allow . 185 | one-click 186 | ``` 187 | 188 | [![asciicast](https://asciinema.org/a/b3QyYyfxtiihnElhJDafu1quK.svg)](https://asciinema.org/a/b3QyYyfxtiihnElhJDafu1quK) 189 | 190 | After successfully compiling once, if you want to start yakite again, you only need to run: 191 | 192 | ```fish 193 | cd ~/git.workspace/yakite 194 | yakite-daemon:run 195 | ``` 196 | 197 | ## TODO 198 | 199 | - [x] Changesets 200 | - [x] Implementing non-node package version management through ChangesetsExtra 201 | - [x] Github Actions 202 | - [x] Version 203 | - [x] Release 204 | - [ ] Mouse support(Yabai's window does not have the moving and resizing attributes. To judge the behavior of mouse movement or window resizing, additional information must be obtained from the monitoring of mouse events.) 205 | - [ ] Trigger recalculation of available screen space after dock and global menu are hidden 206 | - [ ] Communicates directly with yabai via socket(the current communication is via command line forwarding) 207 | - [ ] Wait for bun to resolve the compatibility issue first and switch the javascript runtime to bun 208 | - [ ] If necessary, wait for GPT to evolve and have it transpile all code to rust, ditching scripts and using native binaries. 209 | 210 | [yabai]: https://github.com/koekeishiya/yabai 211 | [krohnkite]: https://github.com/esjeon/krohnkite 212 | [bismuth]: https://github.com/Bismuth-Forge/bismuth 213 | [krohnkite-core]: https://github.com/esjeon/krohnkite 214 | [nix-darwin]: https://github.com/LnL7/nix-darwin/ 215 | [nix-direnv]: https://github.com/nix-community/nix-direnv 216 | [nix-install-macos]: https://nixos.org/download#nix-install-macos 217 | 218 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A dynamic tiled window management that bridges the gap between yabai and krohnkite"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | devenv.url = "github:cachix/devenv"; 7 | nix2container.url = "github:nlewo/nix2container"; 8 | nix2container.inputs.nixpkgs.follows = "nixpkgs"; 9 | mk-shell-bin.url = "github:rrbutani/nix-mk-shell-bin"; 10 | fenix = { 11 | url = "github:nix-community/fenix"; 12 | inputs.nixpkgs.follows = "nixpkgs"; 13 | }; 14 | treefmt-nix.url = "github:numtide/treefmt-nix"; 15 | flake-root.url = "github:srid/flake-root"; 16 | }; 17 | 18 | nixConfig = { 19 | extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw="; 20 | extra-substituters = [ 21 | "https://mirrors.tuna.tsinghua.edu.cn/nix-channels/store" 22 | "https://mirrors.ustc.edu.cn/nix-channels/store" 23 | "https://devenv.cachix.org" 24 | ]; 25 | }; 26 | 27 | outputs = inputs @ { 28 | flake-parts, 29 | nixpkgs, 30 | ... 31 | }: 32 | flake-parts.lib.mkFlake {inherit inputs;} { 33 | imports = [inputs.devenv.flakeModule inputs.treefmt-nix.flakeModule inputs.flake-root.flakeModule]; 34 | systems = [ 35 | "x86_64-linux" 36 | "i686-linux" 37 | "x86_64-darwin" 38 | "aarch64-linux" 39 | "aarch64-darwin" 40 | ]; 41 | 42 | perSystem = { 43 | config, 44 | self', 45 | inputs', 46 | pkgs, 47 | system, 48 | ... 49 | }: let 50 | cargoBuildInputs = 51 | pkgs.lib.optionals pkgs.stdenv.isDarwin 52 | (with pkgs.darwin.apple_sdk; [ 53 | frameworks.Security 54 | frameworks.CoreServices 55 | ]); 56 | in { 57 | # Per-system attributes can be defined here. The self' and inputs' 58 | # module parameters provide easy access to attributes of the same 59 | # system. 60 | devenv.shells.default = { 61 | name = "yakite mono repo"; 62 | 63 | imports = [ 64 | # This is just like the imports in devenv.nix. 65 | # See https://devenv.sh/guides/using-with-flake-parts/#import-a-devenv-module 66 | # ./devenv-foo.nix 67 | ]; 68 | 69 | # https://devenv.sh/reference/options/ 70 | packages = with pkgs; 71 | [bun] 72 | ++ [zellij] 73 | ++ [pkg-config libsodium cmake] # zeromq 74 | ++ [nodePackages.pnpm npm-check-updates]; 75 | 76 | # https://devenv.sh/basics/ 77 | env = { 78 | GREET = "🛠️ Let's hack 🧑🏻‍💻"; 79 | }; 80 | 81 | # https://devenv.sh/scripts/ 82 | scripts.hello.exec = "echo $GREET"; 83 | scripts.up.exec = "ncu -i"; 84 | 85 | scripts."krohnkite-core:build".exec = '' 86 | cd $DEVENV_ROOT/apps/krohnkite-core 87 | pnpm build 88 | ''; 89 | scripts."yakite-yabai:build".exec = '' 90 | cd $DEVENV_ROOT/apps/yakite-yabai 91 | pnpm build 92 | ''; 93 | scripts."yakite-yabai:test".exec = '' 94 | cd $DEVENV_ROOT/apps/yakite-yabai 95 | pnpm test 96 | ''; 97 | scripts."yakite-bridge:build".exec = '' 98 | cd $DEVENV_ROOT/apps/yakite-bridge 99 | pnpm build 100 | ''; 101 | scripts."yakite-daemon:build".exec = '' 102 | cd $DEVENV_ROOT/apps/yakite-daemon 103 | pnpm build 104 | ''; 105 | scripts."yakite-daemon:run".exec = '' 106 | cd $DEVENV_ROOT/apps/yakite-daemon 107 | ./dist/bin/yakite-daemon.js 108 | ''; 109 | scripts."yakite:build".exec = '' 110 | cd $DEVENV_ROOT/apps/yakite 111 | cargo build --release 112 | ''; 113 | scripts."yakite-toast:build".exec = '' 114 | cd $DEVENV_ROOT/apps/yakite-toast/src 115 | mkdir ../dist 116 | /usr/bin/clang -framework Cocoa -target arm64-apple-macos11 yakite-toast.m -o ../dist/yakite-toast-arm64 117 | /usr/bin/clang -framework Cocoa -arch x86_64 yakite-toast.m -o ../dist/yakite-toast-x86_64 118 | 119 | /usr/bin/lipo ../dist/yakite-toast-arm64 ../dist/yakite-toast-x86_64 -create -output ../dist/yakite-toast \ 120 | && bunx chalk-cli green bold 'Build Done!' \ 121 | || bunx chalk-cli red bold 'Build Failed!' 122 | 123 | ''; 124 | scripts."yakite-toast:test".exec = '' 125 | cd $DEVENV_ROOT/apps/yakite-toast/dist 126 | ./yakite-toast --time 3.14 --text "Tile Layout" 127 | ''; 128 | scripts."all:build".exec = '' 129 | cd $DEVENV_ROOT 130 | sh -c $'sleep 0.314;zellij -s yakite' & 131 | sh -c $'sleep 0.314;zellij -s yakite action write-chars "zellij run -- yakite-toast:build\n"' & 132 | sh -c $'sleep 0.314;zellij -s yakite action write-chars "zellij run -- yakite:build\n"' & 133 | sh -c $'sleep 0.314;zellij -s yakite action write-chars "zellij run -- pnpm --recursiv build\n"' & 134 | zellij --session yakite || zellij attach yakite 135 | ''; 136 | scripts."one-click".exec = '' 137 | pnpm i && all:build 138 | yakite-daemon:run 139 | ''; 140 | 141 | scripts."git-user:setup".exec = '' 142 | git config user.name I-Want-ToBelieve 143 | git config user.email i.want.tobelieve.dev@gmail.com 144 | ''; 145 | 146 | enterShell = '' 147 | hello 148 | ''; 149 | 150 | # https://devenv.sh/languages/ 151 | # yakite-daemon yakite-bridge krohnkite-core yabai 152 | languages.javascript = { 153 | enable = true; 154 | package = pkgs.nodejs_20; # latest lts version 155 | }; 156 | languages.typescript = {enable = true;}; 157 | 158 | # yakite 159 | languages.rust = { 160 | enable = true; 161 | channel = "stable"; 162 | components = ["rustc" "cargo" "clippy" "rustfmt" "rust-analyzer"]; 163 | }; 164 | 165 | # Make diffs fantastic 166 | difftastic.enable = true; 167 | 168 | # https://devenv.sh/pre-commit-hooks/ 169 | pre-commit.hooks = { 170 | # commons 171 | editorconfig-checker.enable = true; 172 | 173 | # configs 174 | yamllint.enable = true; 175 | 176 | # nix 177 | alejandra.enable = true; 178 | 179 | # javascript 180 | prettier.enable = false; 181 | eslint.enable = true; 182 | 183 | # rust 184 | rustfmt.enable = true; 185 | clippy.enable = true; 186 | 187 | # objective-c 188 | clang-format.enable = false; 189 | }; 190 | 191 | # Plugin configuration 192 | pre-commit.settings = {yamllint.relaxed = true;}; 193 | }; 194 | 195 | treefmt.config = { 196 | inherit (config.flake-root) projectRootFile; 197 | # This is the default, and can be overriden. 198 | package = pkgs.treefmt; 199 | 200 | # formats .nix files 201 | programs.alejandra.enable = true; 202 | }; 203 | }; 204 | flake = { 205 | # The usual flake attributes can be defined here, including system- 206 | # agnostic ones like nixosModule and system-enumerating ones, although 207 | # those are more easily expressed in perSystem. 208 | }; 209 | }; 210 | } 211 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/utils.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { clip } from '../../utils/func' 7 | import { Rect, type RectDelta } from '../../utils/rect' 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 10 | export default class LayoutUtils { 11 | /** 12 | * Split a (virtual) line into weighted lines w/ gaps. 13 | * @param length The length of the line to be splitted 14 | * @param weights The weight of each part 15 | * @param gap The size of gap b/w parts 16 | * @returns An array of parts: [begin, length] 17 | */ 18 | public static splitWeighted ( 19 | [begin, length]: [number, number], 20 | weights: number[], 21 | gap: number 22 | ): Array<[number, number]> { 23 | gap = gap ?? 0 24 | 25 | const n = weights.length 26 | const actualLength = length - (n - 1) * gap 27 | const weightSum = weights.reduce((sum, weight) => sum + weight, 0) 28 | 29 | let weightAcc = 0 30 | return weights.map((weight, i) => { 31 | const partBegin = (actualLength * weightAcc) / weightSum + i * gap 32 | const partLength = (actualLength * weight) / weightSum 33 | weightAcc += weight 34 | return [begin + Math.floor(partBegin), Math.floor(partLength)] 35 | }) 36 | } 37 | 38 | /** 39 | * Split an area into multiple parts based on weight. 40 | * @param area The area to be splitted 41 | * @param weights The weight of each part 42 | * @param gap The size of gaps b/w parts 43 | * @param horizontal If true, split horizontally. Otherwise, vertically. 44 | */ 45 | public static splitAreaWeighted ( 46 | area: Rect, 47 | weights: number[], 48 | gap?: number, 49 | horizontal?: boolean 50 | ): Rect[] { 51 | gap = gap ?? 0 52 | horizontal = horizontal ?? false 53 | 54 | const line: [number, number] = horizontal 55 | ? [area.x, area.width] 56 | : [area.y, area.height] 57 | const parts = LayoutUtils.splitWeighted(line, weights, gap) 58 | 59 | return parts.map(([begin, length]) => 60 | horizontal 61 | ? new Rect(begin, area.y, length, area.height) 62 | : new Rect(area.x, begin, area.width, length) 63 | ) 64 | } 65 | 66 | /** 67 | * Split an area into two based on weight. 68 | * @param area The area to be splitted 69 | * @param weight The weight of the left/upper part. 70 | * @param gap The size of gaps b/w parts 71 | * @param horizontal If true, split horizontally. Otherwise, vertically. 72 | */ 73 | public static splitAreaHalfWeighted ( 74 | area: Rect, 75 | weight: number, 76 | gap?: number, 77 | horizontal?: boolean 78 | ): Rect[] { 79 | return LayoutUtils.splitAreaWeighted( 80 | area, 81 | [weight, 1 - weight], 82 | gap, 83 | horizontal 84 | ) 85 | } 86 | 87 | /** 88 | * Recalculate the weights of subareas of the line, based on size change. 89 | * @param line The line being aplitted 90 | * @param weights The weight of each part 91 | * @param gap The gap size b/w parts 92 | * @param target The index of the part being changed. 93 | * @param deltaFw The amount of growth towards the origin. 94 | * @param deltaBw The amount of growth towards the infinity. 95 | */ 96 | public static adjustWeights ( 97 | [begin, length]: [number, number], 98 | weights: number[], 99 | gap: number, 100 | target: number, 101 | deltaFw: number, 102 | deltaBw: number 103 | ): number[] { 104 | // TODO: configurable min length? 105 | const minLength = 1 106 | 107 | const parts = this.splitWeighted([begin, length], weights, gap) 108 | const [targetBase, targetLength] = parts[target] 109 | 110 | /* apply backward delta */ 111 | if (target > 0 && deltaBw !== 0) { 112 | const neighbor = target - 1 113 | const [neighborBase, neighborLength] = parts[neighbor] 114 | 115 | /* limit delta to prevent squeezing windows */ 116 | const delta = clip( 117 | deltaBw, 118 | minLength - targetLength, 119 | neighborLength - minLength 120 | ) 121 | 122 | parts[target] = [targetBase - delta, targetLength + delta] 123 | parts[neighbor] = [neighborBase, neighborLength - delta] 124 | } 125 | 126 | /* apply forward delta */ 127 | if (target < parts.length - 1 && deltaFw !== 0) { 128 | const neighbor = target + 1 129 | const [neighborBase, neighborLength] = parts[neighbor] 130 | 131 | /* limit delta to prevent squeezing windows */ 132 | const delta = clip( 133 | deltaFw, 134 | minLength - targetLength, 135 | neighborLength - minLength 136 | ) 137 | 138 | parts[target] = [targetBase, targetLength + delta] 139 | parts[neighbor] = [neighborBase + delta, neighborLength - delta] 140 | } 141 | 142 | return LayoutUtils.calculateWeights(parts) 143 | } 144 | 145 | /** 146 | * Recalculate weights of subareas splitting the given area, based on size change. 147 | * @param area The area being splitted 148 | * @param weights The weight of each part 149 | * @param gap The gap size b/w parts 150 | * @param target The index of the part being changed. 151 | * @param delta The changes in dimension of the target 152 | * @param horizontal If true, calculate horizontal weights, instead of vertical. 153 | */ 154 | public static adjustAreaWeights ( 155 | area: Rect, 156 | weights: number[], 157 | gap: number, 158 | target: number, 159 | delta: RectDelta, 160 | horizontal?: boolean 161 | ): number[] { 162 | const line: [number, number] = horizontal 163 | ? [area.x, area.width] 164 | : [area.y, area.height] 165 | const [deltaFw, deltaBw] = horizontal 166 | ? [delta.east, delta.west] 167 | : [delta.south, delta.north] 168 | return LayoutUtils.adjustWeights( 169 | line, 170 | weights, 171 | gap, 172 | target, 173 | deltaFw, 174 | deltaBw 175 | ) 176 | } 177 | 178 | /** 179 | * Recalculate weights of two areas splitting the given area, based on size change. 180 | * @param area The area being splitted 181 | * @param weight The weight of the left/upper part 182 | * @param gap The gap size b/w parts 183 | * @param target The index of the part being changed. 184 | * @param delta The changes in dimension of the target 185 | * @param horizontal If true, calculate horizontal weights, instead of vertical. 186 | */ 187 | public static adjustAreaHalfWeights ( 188 | area: Rect, 189 | weight: number, 190 | gap: number, 191 | target: number, 192 | delta: RectDelta, 193 | horizontal?: boolean 194 | ): number { 195 | const weights = [weight, 1 - weight] 196 | const newWeights = LayoutUtils.adjustAreaWeights( 197 | area, 198 | weights, 199 | gap, 200 | target, 201 | delta, 202 | horizontal 203 | ) 204 | return newWeights[0] 205 | } 206 | 207 | /** 208 | * Calculates the weights of all parts, splitting a line. 209 | */ 210 | public static calculateWeights (parts: Array<[number, number]>): number[] { 211 | const totalLength = parts.reduce((acc, [_base, length]) => acc + length, 0) 212 | return parts.map(([_base, length]) => length / totalLength) 213 | } 214 | 215 | /** 216 | * Calculates the weights of all parts, splitting an area. 217 | */ 218 | public static calculateAreaWeights ( 219 | _area: Rect, 220 | geometries: Rect[], 221 | gap?: number, 222 | horizontal?: boolean 223 | ): number[] { 224 | gap = gap ?? 0 225 | horizontal = horizontal ?? false 226 | 227 | // const line = horizontal ? area.width : area.height; 228 | const parts: Array<[number, number]> = horizontal 229 | ? geometries.map((geometry) => [geometry.x, geometry.width]) 230 | : geometries.map((geometry) => [geometry.y, geometry.height]) 231 | return LayoutUtils.calculateWeights(parts) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/three-column.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type WindowsLayout } from '.' 7 | import LayoutUtils from './utils' 8 | 9 | import { WindowState, type IEngineWindow } from '../window' 10 | 11 | import { 12 | type Action, 13 | DecreaseLayoutMasterAreaSize, 14 | DecreaseMasterAreaWindowCount, 15 | IncreaseLayoutMasterAreaSize, 16 | IncreaseMasterAreaWindowCount 17 | } from '../../action' 18 | 19 | import { partitionArrayBySizes, clip, slide } from '../../utils/func' 20 | import { type Rect, type RectDelta } from '../../utils/rect' 21 | 22 | import { type IWindowsManager } from '../../manager' 23 | import { type IEngine } from '..' 24 | import { type Config } from '@/config' 25 | 26 | export default class ThreeColumnLayout implements WindowsLayout { 27 | public static readonly MIN_MASTER_RATIO = 0.2 28 | public static readonly MAX_MASTER_RATIO = 0.75 29 | public static readonly id = 'ThreeColumnLayout' 30 | public readonly classID = ThreeColumnLayout.id 31 | public readonly name = 'Three-Column Layout' 32 | public readonly icon = 'bismuth-column' 33 | 34 | public get hint (): string { 35 | return String(this.masterSize) 36 | } 37 | 38 | private masterRatio: number 39 | private masterSize: number 40 | 41 | private readonly config: Config 42 | 43 | constructor (config: Config) { 44 | this.config = config 45 | this.masterRatio = 0.6 46 | this.masterSize = 1 47 | } 48 | 49 | public adjust ( 50 | area: Rect, 51 | tiles: IEngineWindow[], 52 | basis: IEngineWindow, 53 | delta: RectDelta 54 | ): void { 55 | const basisIndex = tiles.indexOf(basis) 56 | if (basisIndex < 0) { 57 | return 58 | } 59 | 60 | if (tiles.length === 0) { 61 | /* no tiles */ 62 | 63 | } else if (tiles.length <= this.masterSize) { 64 | /* one column */ 65 | LayoutUtils.adjustAreaWeights( 66 | area, 67 | tiles.map((tile) => tile.weight), 68 | this.config.gaps.tileLayout, 69 | tiles.indexOf(basis), 70 | delta 71 | ).forEach((newWeight, i) => (tiles[i].weight = newWeight * tiles.length)) 72 | } else if (tiles.length === this.masterSize + 1) { 73 | /* two columns */ 74 | 75 | /* adjust master-stack ratio */ 76 | this.masterRatio = LayoutUtils.adjustAreaHalfWeights( 77 | area, 78 | this.masterRatio, 79 | this.config.gaps.tileLayout, 80 | basisIndex < this.masterSize ? 0 : 1, 81 | delta, 82 | true 83 | ) 84 | 85 | /* adjust master tile weights */ 86 | if (basisIndex < this.masterSize) { 87 | const masterTiles = tiles.slice(0, -1) 88 | LayoutUtils.adjustAreaWeights( 89 | area, 90 | masterTiles.map((tile) => tile.weight), 91 | this.config.gaps.tileLayout, 92 | basisIndex, 93 | delta 94 | ).forEach( 95 | (newWeight, i) => 96 | (masterTiles[i].weight = newWeight * masterTiles.length) 97 | ) 98 | } 99 | } else if (tiles.length > this.masterSize + 1) { 100 | /* three columns */ 101 | let basisGroup 102 | if (basisIndex < this.masterSize) { 103 | /* master */ 104 | basisGroup = 1 105 | } else if ( 106 | basisIndex < Math.floor((this.masterSize + tiles.length) / 2) 107 | ) { 108 | /* R-stack */ 109 | basisGroup = 2 110 | } else { 111 | basisGroup = 0 /* L-stack */ 112 | } 113 | 114 | /* adjust master-stack ratio */ 115 | const stackRatio = 1 - this.masterRatio 116 | const newRatios = LayoutUtils.adjustAreaWeights( 117 | area, 118 | [stackRatio, this.masterRatio, stackRatio], 119 | this.config.gaps.tileLayout, 120 | basisGroup, 121 | delta, 122 | true 123 | ) 124 | const newMasterRatio = newRatios[1] 125 | const newStackRatio = basisGroup === 0 ? newRatios[0] : newRatios[2] 126 | this.masterRatio = newMasterRatio / (newMasterRatio + newStackRatio) 127 | 128 | /* adjust tile weight */ 129 | const rstackNumTile = Math.floor((tiles.length - this.masterSize) / 2) 130 | const [masterTiles, rstackTiles, lstackTiles] = 131 | partitionArrayBySizes(tiles, [ 132 | this.masterSize, 133 | rstackNumTile 134 | ]) 135 | const groupTiles = [lstackTiles, masterTiles, rstackTiles][basisGroup] 136 | LayoutUtils.adjustAreaWeights( 137 | area /* we only need height */, 138 | groupTiles.map((tile) => tile.weight), 139 | this.config.gaps.tileLayout, 140 | groupTiles.indexOf(basis), 141 | delta 142 | ).forEach( 143 | (newWeight, i) => (groupTiles[i].weight = newWeight * groupTiles.length) 144 | ) 145 | } 146 | } 147 | 148 | public apply ( 149 | _controller: IWindowsManager, 150 | tileables: IEngineWindow[], 151 | area: Rect 152 | ): void { 153 | /* Tile all tileables */ 154 | tileables.forEach((tileable) => (tileable.state = WindowState.Tiled)) 155 | const tiles = tileables 156 | 157 | if (tiles.length <= this.masterSize) { 158 | /* only master */ 159 | LayoutUtils.splitAreaWeighted( 160 | area, 161 | tiles.map((tile) => tile.weight), 162 | this.config.gaps.tileLayout 163 | ).forEach((tileArea, i) => (tiles[i].geometry = tileArea)) 164 | } else if (tiles.length === this.masterSize + 1) { 165 | /* master & R-stack (only 1 window in stack) */ 166 | const [masterArea, stackArea] = LayoutUtils.splitAreaHalfWeighted( 167 | area, 168 | this.masterRatio, 169 | this.config.gaps.tileLayout, 170 | true 171 | ) 172 | 173 | const masterTiles = tiles.slice(0, this.masterSize) 174 | LayoutUtils.splitAreaWeighted( 175 | masterArea, 176 | masterTiles.map((tile) => tile.weight), 177 | this.config.gaps.tileLayout 178 | ).forEach((tileArea, i) => (masterTiles[i].geometry = tileArea)) 179 | 180 | tiles[tiles.length - 1].geometry = stackArea 181 | } else if (tiles.length > this.masterSize + 1) { 182 | /* L-stack & master & R-stack */ 183 | const stackRatio = 1 - this.masterRatio 184 | 185 | /** Areas allocated to L-stack, master, and R-stack */ 186 | const groupAreas = LayoutUtils.splitAreaWeighted( 187 | area, 188 | [stackRatio, this.masterRatio, stackRatio], 189 | this.config.gaps.tileLayout, 190 | true 191 | ) 192 | 193 | const rstackSize = Math.floor((tiles.length - this.masterSize) / 2) 194 | const [masterTiles, rstackTiles, lstackTiles] = 195 | partitionArrayBySizes(tiles, [ 196 | this.masterSize, 197 | rstackSize 198 | ]); 199 | [lstackTiles, masterTiles, rstackTiles].forEach((groupTiles, group) => { 200 | LayoutUtils.splitAreaWeighted( 201 | groupAreas[group], 202 | groupTiles.map((tile) => tile.weight), 203 | this.config.gaps.tileLayout 204 | ).forEach((tileArea, i) => (groupTiles[i].geometry = tileArea)) 205 | }) 206 | } 207 | } 208 | 209 | public clone (): WindowsLayout { 210 | const other = new ThreeColumnLayout(this.config) 211 | other.masterRatio = this.masterRatio 212 | other.masterSize = this.masterSize 213 | return other 214 | } 215 | 216 | public executeAction (engine: IEngine, action: Action): void { 217 | if (action instanceof IncreaseMasterAreaWindowCount) { 218 | this.resizeMaster(engine, +1) 219 | } else if (action instanceof DecreaseMasterAreaWindowCount) { 220 | this.resizeMaster(engine, -1) 221 | } else if (action instanceof DecreaseLayoutMasterAreaSize) { 222 | this.masterRatio = clip( 223 | slide(this.masterRatio, -0.05), 224 | ThreeColumnLayout.MIN_MASTER_RATIO, 225 | ThreeColumnLayout.MAX_MASTER_RATIO 226 | ) 227 | } else if (action instanceof IncreaseLayoutMasterAreaSize) { 228 | this.masterRatio = clip( 229 | slide(this.masterRatio, +0.05), 230 | ThreeColumnLayout.MIN_MASTER_RATIO, 231 | ThreeColumnLayout.MAX_MASTER_RATIO 232 | ) 233 | } else { 234 | action.executeWithoutLayoutOverride() 235 | } 236 | } 237 | 238 | public toString (): string { 239 | return `ThreeColumnLayout(nmaster=${this.masterSize})` 240 | } 241 | 242 | private resizeMaster (engine: IEngine, step: -1 | 1): void { 243 | this.masterSize = clip(this.masterSize + step, 1, 10) 244 | engine.showLayoutNotification() 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/layouts/part.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import LayoutUtils from './utils' 7 | 8 | import { type IEngineWindow } from '../window' 9 | 10 | import { Rect, RectDelta } from '../../utils/rect' 11 | import { type Config } from '@/config' 12 | 13 | export interface ILayoutPart { 14 | adjust: ( 15 | area: Rect, 16 | tiles: IEngineWindow[], 17 | basis: IEngineWindow, 18 | delta: RectDelta 19 | ) => RectDelta 20 | apply: (area: Rect, tiles: IEngineWindow[]) => Rect[] 21 | } 22 | 23 | export class FillLayoutPart implements ILayoutPart { 24 | public adjust ( 25 | area: Rect, 26 | tiles: IEngineWindow[], 27 | basis: IEngineWindow, 28 | delta: RectDelta 29 | ): RectDelta { 30 | /* do nothing */ 31 | return delta 32 | } 33 | 34 | public apply (area: Rect, tiles: IEngineWindow[]): Rect[] { 35 | return tiles.map((_tile) => { 36 | return area 37 | }) 38 | } 39 | } 40 | 41 | export class HalfSplitLayoutPart 42 | implements ILayoutPart { 43 | /** the rotation angle for this part. 44 | * 45 | * | angle | direction | primary | 46 | * | ----- | ---------- | ------- | 47 | * | 0 | horizontal | left | 48 | * | 90 | vertical | top | 49 | * | 180 | horizontal | right | 50 | * | 270 | vertical | bottom | 51 | */ 52 | public angle: 0 | 90 | 180 | 270 53 | 54 | public gap: number 55 | public primarySize: number 56 | public ratio: number 57 | 58 | private get horizontal (): boolean { 59 | return this.angle === 0 || this.angle === 180 60 | } 61 | 62 | private get reversed (): boolean { 63 | return this.angle === 180 || this.angle === 270 64 | } 65 | 66 | constructor (public primary: L, public secondary: R) { 67 | this.angle = 0 68 | this.gap = 0 69 | this.primarySize = 1 70 | this.ratio = 0.5 71 | } 72 | 73 | public adjust ( 74 | area: Rect, 75 | tiles: IEngineWindow[], 76 | basis: IEngineWindow, 77 | delta: RectDelta 78 | ): RectDelta { 79 | const basisIndex = tiles.indexOf(basis) 80 | if (basisIndex < 0) { 81 | return delta 82 | } 83 | 84 | if (tiles.length <= this.primarySize) { 85 | /* primary only */ 86 | return this.primary.adjust(area, tiles, basis, delta) 87 | } else if (this.primarySize === 0) { 88 | /* secondary only */ 89 | return this.secondary.adjust(area, tiles, basis, delta) 90 | } else { 91 | /* both parts */ 92 | 93 | /** which part to adjust. 0 = primary, 1 = secondary */ 94 | const targetIndex = basisIndex < this.primarySize ? 0 : 1 95 | 96 | if (targetIndex === /* primary */ 0) { 97 | delta = this.primary.adjust( 98 | area, 99 | tiles.slice(0, this.primarySize), 100 | basis, 101 | delta 102 | ) 103 | } else { 104 | delta = this.secondary.adjust( 105 | area, 106 | tiles.slice(this.primarySize), 107 | basis, 108 | delta 109 | ) 110 | } 111 | 112 | this.ratio = LayoutUtils.adjustAreaHalfWeights( 113 | area, 114 | this.reversed ? 1 - this.ratio : this.ratio, 115 | this.gap, 116 | this.reversed ? 1 - targetIndex : targetIndex, 117 | delta, 118 | this.horizontal 119 | ) 120 | if (this.reversed) { 121 | this.ratio = 1 - this.ratio 122 | } 123 | 124 | switch (this.angle * 10 + targetIndex + 1) { 125 | case 1: /* 0, Primary */ 126 | case 1802 /* 180, Secondary */: 127 | return new RectDelta(0, delta.west, delta.south, delta.north) 128 | case 2: 129 | case 1801: 130 | return new RectDelta(delta.east, 0, delta.south, delta.north) 131 | case 901: 132 | case 2702: 133 | return new RectDelta(delta.east, delta.west, 0, delta.north) 134 | case 902: 135 | case 2701: 136 | return new RectDelta(delta.east, delta.west, delta.south, 0) 137 | } 138 | return delta 139 | } 140 | } 141 | 142 | public apply (area: Rect, tiles: IEngineWindow[]): Rect[] { 143 | if (tiles.length <= this.primarySize) { 144 | /* primary only */ 145 | return this.primary.apply(area, tiles) 146 | } else if (this.primarySize === 0) { 147 | /* secondary only */ 148 | return this.secondary.apply(area, tiles) 149 | } else { 150 | /* both parts */ 151 | const reversed = this.reversed 152 | const ratio = reversed ? 1 - this.ratio : this.ratio 153 | const [area1, area2] = LayoutUtils.splitAreaHalfWeighted( 154 | area, 155 | ratio, 156 | this.gap, 157 | this.horizontal 158 | ) 159 | const result1 = this.primary.apply( 160 | reversed ? area2 : area1, 161 | tiles.slice(0, this.primarySize) 162 | ) 163 | const result2 = this.secondary.apply( 164 | reversed ? area1 : area2, 165 | tiles.slice(this.primarySize) 166 | ) 167 | return result1.concat(result2) 168 | } 169 | } 170 | } 171 | 172 | export class StackLayoutPart implements ILayoutPart { 173 | public gap: number 174 | 175 | private readonly config: Config 176 | 177 | constructor (config: Config) { 178 | this.config = config 179 | this.gap = 0 180 | } 181 | 182 | public adjust ( 183 | area: Rect, 184 | tiles: IEngineWindow[], 185 | basis: IEngineWindow, 186 | delta: RectDelta 187 | ): RectDelta { 188 | const weights = LayoutUtils.adjustAreaWeights( 189 | area, 190 | tiles.map((tile) => tile.weight), 191 | this.config.gaps.tileLayout, 192 | tiles.indexOf(basis), 193 | delta, 194 | false 195 | ) 196 | 197 | weights.forEach((weight, i) => { 198 | tiles[i].weight = weight * tiles.length 199 | }) 200 | 201 | const idx = tiles.indexOf(basis) 202 | return new RectDelta( 203 | delta.east, 204 | delta.west, 205 | idx === tiles.length - 1 ? delta.south : 0, 206 | idx === 0 ? delta.north : 0 207 | ) 208 | } 209 | 210 | public apply (area: Rect, tiles: IEngineWindow[]): Rect[] { 211 | const weights = tiles.map((tile) => tile.weight) 212 | return LayoutUtils.splitAreaWeighted(area, weights, this.gap) 213 | } 214 | } 215 | 216 | export class RotateLayoutPart implements ILayoutPart { 217 | constructor (public inner: T, public angle: 0 | 90 | 180 | 270 = 0) {} 218 | 219 | public adjust ( 220 | area: Rect, 221 | tiles: IEngineWindow[], 222 | basis: IEngineWindow, 223 | delta: RectDelta 224 | ): RectDelta { 225 | // let area = area, delta = delta; 226 | switch (this.angle) { 227 | case 0: 228 | break 229 | case 90: 230 | area = new Rect(area.y, area.x, area.height, area.width) 231 | delta = new RectDelta(delta.south, delta.north, delta.east, delta.west) 232 | break 233 | case 180: 234 | delta = new RectDelta(delta.west, delta.east, delta.south, delta.north) 235 | break 236 | case 270: 237 | area = new Rect(area.y, area.x, area.height, area.width) 238 | delta = new RectDelta(delta.north, delta.south, delta.east, delta.west) 239 | break 240 | } 241 | 242 | delta = this.inner.adjust(area, tiles, basis, delta) 243 | 244 | switch (this.angle) { 245 | case 0: 246 | // No adjustment needed 247 | break 248 | case 90: 249 | delta = new RectDelta(delta.south, delta.north, delta.east, delta.west) 250 | break 251 | case 180: 252 | delta = new RectDelta(delta.west, delta.east, delta.south, delta.north) 253 | break 254 | case 270: 255 | delta = new RectDelta(delta.north, delta.south, delta.east, delta.west) 256 | break 257 | } 258 | return delta 259 | } 260 | 261 | public apply (area: Rect, tiles: IEngineWindow[]): Rect[] { 262 | switch (this.angle) { 263 | case 0: 264 | break 265 | case 90: 266 | area = new Rect(area.y, area.x, area.height, area.width) 267 | break 268 | case 180: 269 | break 270 | case 270: 271 | area = new Rect(area.y, area.x, area.height, area.width) 272 | break 273 | } 274 | 275 | const innerResult = this.inner.apply(area, tiles) 276 | 277 | switch (this.angle) { 278 | case 0: 279 | return innerResult 280 | case 90: 281 | return innerResult.map((g) => new Rect(g.y, g.x, g.height, g.width)) 282 | case 180: 283 | return innerResult.map((g) => { 284 | const rx = g.x - area.x 285 | const newX = area.x + area.width - (rx + g.width) 286 | return new Rect(newX, g.y, g.width, g.height) 287 | }) 288 | case 270: 289 | return innerResult.map((g) => { 290 | const rx = g.x - area.x 291 | const newY = area.x + area.width - (rx + g.width) 292 | return new Rect(g.y, newY, g.height, g.width) 293 | }) 294 | } 295 | } 296 | 297 | public rotate (amount: -90 | 90): void { 298 | // -90 | 0 | 90 | 180 | 270 | 360 299 | let angle = this.angle + amount 300 | if (angle < 0) { 301 | angle = 270 302 | } else if (angle >= 360) { 303 | angle = 0 304 | } 305 | 306 | this.angle = angle as 0 | 90 | 180 | 270 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /apps/krohnkite-core/src/engine/window.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018-2019 Eon S. Jeon 2 | // SPDX-FileCopyrightText: 2021 Mikhail Zolotukhin 3 | // 4 | // SPDX-License-Identifier: MIT 5 | 6 | import { type IBridgeWindow } from '../bridge/window' 7 | import { type IBridgeSurface } from '../bridge/surface' 8 | 9 | import { type Rect, RectDelta } from '../utils/rect' 10 | 11 | import { type Config } from '@/config' 12 | import { type Logger } from '@/types/logger.types' 13 | 14 | export enum WindowState { 15 | /** 16 | * Initial value 17 | */ 18 | Unmanaged, 19 | 20 | /** 21 | * Script-external state - overrides internal state 22 | */ 23 | NativeFullscreen, 24 | NativeMaximized, 25 | 26 | /** 27 | * Script-internal state 28 | */ 29 | Floating, 30 | Maximized, 31 | Tiled, 32 | TiledAfloat, 33 | Undecided, 34 | } 35 | 36 | /** 37 | * Window with the convenient for the Engine Interface 38 | */ 39 | export interface IEngineWindow { 40 | /** 41 | * Window unique id 42 | */ 43 | readonly id: string 44 | 45 | /** 46 | * If this window ***can be*** tiled by layout. 47 | */ 48 | readonly tileable: boolean 49 | 50 | /** 51 | * If this window is ***already*** tiled, thus a part of the current layout. 52 | */ 53 | readonly tiled: boolean 54 | 55 | /** 56 | * If this window is floating, thus its geometry is not tightly managed. 57 | */ 58 | readonly floating: boolean 59 | 60 | /** 61 | * Whether the window is shaded (collapsed to the title bar) 62 | */ 63 | readonly shaded: boolean 64 | 65 | /** 66 | * Low-level implementation, usable for Driver 67 | */ 68 | readonly window: IBridgeWindow 69 | 70 | /** 71 | * Difference between geometry and actual geometry 72 | */ 73 | readonly geometryDelta: RectDelta 74 | 75 | /** 76 | * Actual geometry 77 | */ 78 | readonly actualGeometry: Readonly 79 | 80 | /** 81 | * Whether the window is a dialog window 82 | */ 83 | readonly isDialog: boolean 84 | 85 | /** 86 | * Whether the window should be set to floating state 87 | */ 88 | readonly shouldFloat: boolean 89 | 90 | /** 91 | * Whether the window should be ignored by the script 92 | */ 93 | readonly shouldIgnore: boolean 94 | 95 | /** 96 | * State to which the window was asked to be changed 97 | * previously. This can be the same state, as the current 98 | * one. 99 | */ 100 | readonly statePreviouslyAskedToChangeTo: WindowState 101 | 102 | /** 103 | * Screen number, on which the window is present 104 | */ 105 | readonly screen: number 106 | 107 | /** 108 | * Whether the window is minimized 109 | */ 110 | minimized: boolean 111 | 112 | /** 113 | * Geometry of a window, while in floated state 114 | */ 115 | floatGeometry: Rect 116 | 117 | /** 118 | * Window geometry 119 | */ 120 | geometry: Rect 121 | 122 | /** 123 | * Surface, the window is currently on 124 | */ 125 | surface: IBridgeSurface 126 | 127 | /** 128 | * General state of the window: floating, maximized, tiled etc. 129 | */ 130 | state: WindowState 131 | 132 | /** 133 | * The timestamp when the last time Window was focused. 134 | */ 135 | timestamp: number 136 | 137 | /** 138 | * Window weight. 139 | * TODO: This needs a better explanation. This has something to do with ThreeColumnLayout. 140 | */ 141 | weight: number 142 | 143 | /** 144 | * Whether the window is visible on concrete surface 145 | * @param surface the surface visibility on which is checked 146 | */ 147 | visibleOn: (surface: IBridgeSurface) => boolean 148 | 149 | /** 150 | * Force apply the geometry *immediately*. 151 | * 152 | * This method is a quick hack created for engine#resizeFloat, thus should 153 | * not be used in other places. 154 | */ 155 | forceSetGeometry: (geometry: Rect) => Promise 156 | 157 | /** 158 | * Update changed window properties on the KWin side. 159 | * I.e. make the changes visible to the end user. 160 | */ 161 | commit: () => void 162 | toString: () => string 163 | } 164 | 165 | export class EngineWindow implements IEngineWindow { 166 | public static isTileableState (state: WindowState): boolean { 167 | return ( 168 | state === WindowState.Tiled || 169 | state === WindowState.Maximized || 170 | state === WindowState.TiledAfloat 171 | ) 172 | } 173 | 174 | public static isTiledState (state: WindowState): boolean { 175 | return state === WindowState.Tiled || state === WindowState.Maximized 176 | } 177 | 178 | public static isFloatingState (state: WindowState): boolean { 179 | return state === WindowState.Floating || state === WindowState.TiledAfloat 180 | } 181 | 182 | public readonly id: string 183 | public readonly window: IBridgeWindow 184 | 185 | public get actualGeometry (): Readonly { 186 | return this.window.geometry 187 | } 188 | 189 | public get shouldFloat (): boolean { 190 | return this.window.shouldFloat 191 | } 192 | 193 | public get shouldIgnore (): boolean { 194 | return this.window.shouldIgnore 195 | } 196 | 197 | public get screen (): number { 198 | return this.window.screen 199 | } 200 | 201 | public get minimized (): boolean { 202 | return this.window.minimized 203 | } 204 | 205 | public set minimized (min: boolean) { 206 | this.window.minimized = min 207 | } 208 | 209 | public get tileable (): boolean { 210 | return EngineWindow.isTileableState(this.state) 211 | } 212 | 213 | public get tiled (): boolean { 214 | return EngineWindow.isTiledState(this.state) 215 | } 216 | 217 | public get floating (): boolean { 218 | return EngineWindow.isFloatingState(this.state) 219 | } 220 | 221 | public get geometryDelta (): RectDelta { 222 | return RectDelta.fromRects(this.geometry, this.actualGeometry) 223 | } 224 | 225 | public get shaded (): boolean { 226 | return this.window.shaded 227 | } 228 | 229 | public floatGeometry: Rect 230 | public geometry: Rect 231 | public timestamp: number 232 | 233 | /** 234 | * The current state of the window. 235 | * 236 | * This value affects what and how properties gets committed to the backend. 237 | * 238 | * Avoid comparing this value directly, and use `tileable`, `tiled`, 239 | * `floating` as much as possible. 240 | */ 241 | public get state (): WindowState { 242 | /* external states override the internal state. */ 243 | if (this.window.fullScreen) { 244 | return WindowState.NativeFullscreen 245 | } 246 | if (this.window.maximized) { 247 | return WindowState.NativeMaximized 248 | } 249 | 250 | return this.internalState 251 | } 252 | 253 | public set state (value: WindowState) { 254 | const winState = this.state 255 | this.internalStatePreviouslyAskedToChangeTo = winState 256 | 257 | /* cannot transit to the current state */ 258 | if (winState === value) { 259 | return 260 | } 261 | 262 | if ( 263 | (winState === WindowState.Unmanaged || 264 | EngineWindow.isTileableState(winState)) && 265 | EngineWindow.isFloatingState(value) 266 | ) { 267 | this.shouldCommitFloat = true 268 | } else if ( 269 | EngineWindow.isFloatingState(winState) && 270 | EngineWindow.isTileableState(value) 271 | ) { 272 | /* save the current geometry before leaving floating state */ 273 | this.floatGeometry = this.actualGeometry 274 | } 275 | 276 | this.internalState = value 277 | } 278 | 279 | public get statePreviouslyAskedToChangeTo (): WindowState { 280 | return this.internalStatePreviouslyAskedToChangeTo 281 | } 282 | 283 | public get surface (): IBridgeSurface { 284 | return this.window.surface 285 | } 286 | 287 | public set surface (srf: IBridgeSurface) { 288 | this.window.surface = srf 289 | } 290 | 291 | public get weight (): number { 292 | const srfID = this.window.surface.id 293 | const winWeight: number | undefined = this.weightMap[srfID] 294 | if (winWeight === undefined) { 295 | this.weightMap[srfID] = 1.0 296 | return 1.0 297 | } 298 | return winWeight 299 | } 300 | 301 | public set weight (value: number) { 302 | const srfID = this.window.surface.id 303 | this.weightMap[srfID] = value 304 | } 305 | 306 | public get isDialog (): boolean { 307 | return this.window.isDialog 308 | } 309 | 310 | private internalState: WindowState 311 | private internalStatePreviouslyAskedToChangeTo: WindowState 312 | private shouldCommitFloat: boolean 313 | private weightMap: Record 314 | 315 | private readonly config: Config 316 | 317 | constructor (window: IBridgeWindow, config: Config, private readonly log: Logger) { 318 | this.config = config 319 | 320 | this.id = String(window.id) 321 | this.window = window 322 | this.internalStatePreviouslyAskedToChangeTo = WindowState.Floating 323 | 324 | this.floatGeometry = window.geometry 325 | this.geometry = window.geometry 326 | this.timestamp = 0 327 | 328 | this.internalState = WindowState.Unmanaged 329 | this.shouldCommitFloat = this.shouldFloat 330 | this.weightMap = {} 331 | } 332 | 333 | public async commit (): Promise { 334 | const state = this.state 335 | // this.log.info(["Window#commit", { state: WindowState[state] }]); 336 | switch (state) { 337 | case WindowState.NativeMaximized: 338 | await this.window.commit( 339 | this.window.surface.workingArea, 340 | undefined, 341 | undefined 342 | ) 343 | break 344 | 345 | case WindowState.NativeFullscreen: 346 | await this.window.commit(undefined, undefined, undefined) 347 | break 348 | 349 | case WindowState.Floating: 350 | if (!this.shouldCommitFloat) { 351 | break 352 | } 353 | await this.window.commit( 354 | this.floatGeometry, 355 | false, 356 | this.config.keepFloatAbove 357 | ) 358 | this.shouldCommitFloat = false 359 | break 360 | 361 | case WindowState.Maximized: 362 | await this.window.commit(this.geometry, true, false) 363 | break 364 | 365 | case WindowState.Tiled: 366 | await this.window.commit(this.geometry, this.config.noTileBorder, false) 367 | break 368 | 369 | case WindowState.TiledAfloat: 370 | if (!this.shouldCommitFloat) { 371 | break 372 | } 373 | await this.window.commit( 374 | this.floatGeometry, 375 | false, 376 | this.config.keepFloatAbove 377 | ) 378 | this.shouldCommitFloat = false 379 | break 380 | } 381 | } 382 | 383 | public async forceSetGeometry (geometry: Rect): Promise { 384 | await this.window.commit(geometry) 385 | } 386 | 387 | public visibleOn (srf: IBridgeSurface): boolean { 388 | return this.window.visibleOn(srf) 389 | } 390 | 391 | public toString (): string { 392 | return 'Window(' + String(this.window) + ')' 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /apps/yakite-yabai/src/index.ts: -------------------------------------------------------------------------------- 1 | import { $, type ExecaReturnValue } from 'execa' 2 | import parseJson from 'parse-json' 3 | import { type YabaiSpaces, type YabaiDisplays, type YabaiWindows, type YabaiDisplay, type YabaiSpace, type YabaiWindow, type YabaiFrame } from './yabai.types' 4 | 5 | export default class Yabai { 6 | constructor (public displays: YabaiDisplays, 7 | public spaces: YabaiSpaces, 8 | public windows: YabaiWindows, 9 | public currentDisplay: YabaiDisplay, 10 | public currentSpace: YabaiSpace, 11 | public currentWindow: YabaiWindow 12 | ) { 13 | 14 | } 15 | 16 | public cachedFrame: Record void 21 | onWindowResizeOver?: () => void 22 | onWindowResize?: () => void 23 | }> = {} 24 | 25 | public static async create (): Promise { 26 | const { displays, spaces, windows, currentDisplay, currentSpace, currentWindow } = await this.refresh() 27 | 28 | return new Yabai(displays, spaces, windows, currentDisplay, currentSpace, currentWindow) 29 | } 30 | 31 | public static async refresh (): Promise<{ 32 | displays: YabaiDisplays 33 | spaces: YabaiSpaces 34 | windows: YabaiWindows 35 | currentDisplay: YabaiDisplay 36 | currentSpace: YabaiSpace 37 | currentWindow: YabaiWindow 38 | }> { 39 | const [windows, displays, spaces] = await Promise.all([ 40 | this.windows(), 41 | this.displays(), 42 | this.spaces() 43 | ]) 44 | 45 | const currentWindow = windows.find((it) => it['has-focus']) as YabaiWindow 46 | 47 | const currentDisplay = displays.find((it) => it.index === currentWindow.display) 48 | if (currentDisplay === undefined) throw new Error('no display') 49 | 50 | const currentSpace = spaces.filter((it) => it.display === currentDisplay.index).find((it) => it['has-focus']) 51 | if (currentSpace === undefined) throw new Error('no space') 52 | 53 | return { 54 | displays, spaces, windows, currentDisplay, currentSpace, currentWindow 55 | } 56 | } 57 | 58 | /** 59 | * name 60 | */ 61 | public drop: (() => Promise) | null = null 62 | 63 | public async start (): Promise { 64 | /** 65 | Hello New bing. 66 | Please visit this website to get the definition of yabai signal 67 | https://github.com/koekeishiya/yabai/blob/master/doc/yabai.asciidoc#signal 68 | 69 | Then define all the signals according to the example I gave below. 70 | ```ts 71 | const yakite = '/Users/i.want.to.believe/git.workspaces/js.workspaces/krohnkite-core/apps/yakite/target/release/yakite' 72 | 73 | $`yabai -m signal --add event=application_launched label=yakite-application-launched action='${yakite} event application-launched'` 74 | $`yabai -m signal --add event=window_created label=yakite-window-created action='${yakite} event window-created'` 75 | $`yabai -m signal --add event=window_destroyed label=yakite-window-destroyed action='${yakite} event window-destroyed --env YABAI_WINDOW_ID=$YABAI_WINDOW_ID'` 76 | ``` 77 | 78 | Very good! please complete the definition of all signals given in yabai documentation 79 | */ 80 | let yakite: string 81 | try { 82 | const path = (await $`which yakite`).stdout 83 | yakite = path 84 | } catch { 85 | yakite = `${process.env.DEVENV_ROOT}/apps/yakite/target/release/yakite` 86 | } 87 | 88 | // Define the signals 89 | const signals = [ 90 | { event: 'application_launched', label: 'yakite-application-launched', action: 'event application-launched --env YABAI_PROCESS_ID=$YABAI_PROCESS_ID' }, 91 | { event: 'application_terminated', label: 'yakite-application-terminated', action: 'event application-terminated --env YABAI_PROCESS_ID=$YABAI_PROCESS_ID' }, 92 | { event: 'application_front_switched', label: 'yakite-application-front-switched', action: 'event application-front-switched --env YABAI_PROCESS_ID=$YABAI_PROCESS_ID --env YABAI_RECENT_PROCESS_ID=$YABAI_RECENT_PROCESS_ID' }, 93 | { event: 'application_visible', label: 'yakite-application-visible', action: 'event application-visible --env YABAI_PROCESS_ID=$YABAI_PROCESS_ID' }, 94 | { event: 'application_hidden', label: 'yakite-application-hidden', action: 'event application-hidden --env YABAI_PROCESS_ID=$YABAI_PROCESS_ID' }, 95 | { event: 'window_created', label: 'yakite-window-created', action: 'event window-created --env YABAI_WINDOW_ID=$YABAI_WINDOW_ID' }, 96 | { event: 'window_destroyed', label: 'yakite-window-destroyed', action: 'event window-destroyed --env YABAI_WINDOW_ID=$YABAI_WINDOW_ID' }, 97 | { event: 'window_focused', label: 'yakite-window-focused', action: 'event window-focused --env YABAI_WINDOW_ID=$YABAI_WINDOW_ID' }, 98 | { event: 'window_moved', label: 'yakite-window-moved', action: 'event window-moved --env YABAI_WINDOW_ID=$YABAI_WINDOW_ID' }, 99 | { event: 'window_resized', label: 'yakite-window-resized', action: 'event window-resized --env YABAI_WINDOW_ID=$YABAI_WINDOW_ID' }, 100 | { event: 'window_minimized', label: 'yakite-window-minimized', action: 'event window-minimized --env YABAI_WINDOW_ID=$YABAI_WINDOW_ID' }, 101 | { event: 'window_deminimized', label: 'yakite-window-deminimized', action: 'event window-deminimized --env YABAI_WINDOW_ID=$YABAI_WINDOW_ID' }, 102 | { event: 'window_title_changed', label: 'yakite-window-title-changed', action: 'event window-title-changed --env YABAI_WINDOW_ID=$YABAI_WINDOW_ID' }, 103 | { event: 'space_created', label: 'yakite-space-created', action: 'event space-created --env YABAI_SPACE_ID=$YABAI_SPACE_ID' }, 104 | { event: 'space_destroyed', label: 'yakite-space-destroyed', action: 'event space-destroyed --env YABAI_SPACE_ID=$YABAI_SPACE_ID' }, 105 | { event: 'space_changed', label: 'yakite-space-changed', action: 'event space-changed --env YABAI_SPACE_ID=$YABAI_SPACE_ID --env YABAI_RECENT_SPACE_ID=$YABAI_RECENT_SPACE_ID' }, 106 | { event: 'display_added', label: 'yakite-display-added', action: 'event display-added --env YABAI_DISPLAY_ID=$YABAI_DISPLAY_ID' }, 107 | { event: 'display_removed', label: 'yakite-display-removed', action: 'event display-removed --env YABAI_DISPLAY_ID=$YABAI_DISPLAY_ID' }, 108 | { event: 'display_moved', label: 'yakite-display-moved', action: 'event display-moved --env YABAI_DISPLAY_ID=$YABAI_DISPLAY_ID' }, 109 | { event: 'display_resized', label: 'yakite-display-resized', action: 'event display-resized --env YABAI_DISPLAY_ID=$YABAI_DISPLAY_ID' }, 110 | { event: 'display_changed', label: 'yakite-display-changed', action: 'event display-changed --env YABAI_DISPLAY_ID=$YABAI_DISPLAY_ID --env YABAI_RECENT_DISPLAY_ID=$YABAI_RECENT_DISPLAY_ID' }, 111 | { event: 'mission_control_enter', label: 'yakite-mission-control-enter', action: 'event mission-control-enter' }, 112 | { event: 'mission_control_exit', label: 'yakite-mission-control-exit', action: 'event mission-control-exit' }, 113 | { event: 'dock_did_restart', label: 'yakite-dock-did-restart', action: 'event dock-did-restart' }, 114 | { event: 'menu_bar_hidden_changed', label: 'yakite-menu-bar-hidden-changed', action: 'event menu-bar-hidden-changed' }, 115 | { event: 'dock_did_change_pref', label: 'yakite-dock-did-change-pref', action: 'event dock-did-change-pref' } 116 | ] 117 | 118 | // after message queue server is started 119 | setTimeout(() => { 120 | // Add the signals to yabai 121 | ;(async () => { 122 | for await (const { event, label, action } of signals) { 123 | const cmd = `yabai -m signal --add event=${event} label=${label} action='${yakite} ${action}'` 124 | await $`sh -c ${cmd}` 125 | } 126 | })().catch((e) => { 127 | console.log(e) 128 | }) 129 | }, 0) 130 | 131 | const drop = async (): Promise => { 132 | for await (const { label } of signals) { 133 | await $`yabai -m signal --remove ${label}` 134 | } 135 | } 136 | 137 | this.drop = drop 138 | } 139 | 140 | public static async displays (): Promise { 141 | const displays = (await $`yabai -m query --displays`).stdout 142 | 143 | return parseJson(String(displays)) as unknown as YabaiDisplays 144 | } 145 | 146 | public static async spaces (): Promise { 147 | const spaces = (await $`yabai -m query --spaces`).stdout 148 | 149 | return parseJson(String(spaces)) as unknown as YabaiSpaces 150 | } 151 | 152 | public static async windows (): Promise { 153 | const windows = (await $`yabai -m query --windows`).stdout 154 | 155 | return parseJson(String(windows)) as unknown as YabaiWindows 156 | } 157 | 158 | public async window (id?: string | number): Promise { 159 | const window = (await $`yabai -m query --windows --window ${id ?? ''}`).stdout 160 | 161 | return parseJson(String(window)) as unknown as YabaiWindow 162 | } 163 | 164 | public async display (id: string | number): Promise { 165 | const display = (await $`yabai -m query --displays --display ${id}`).stdout 166 | 167 | return parseJson(String(display)) as unknown as YabaiDisplay 168 | } 169 | 170 | public async space (id: string | number): Promise { 171 | const space = (await $`yabai -m query --spaces --space ${id}`).stdout 172 | 173 | return parseJson(String(space)) as unknown as YabaiSpace 174 | } 175 | 176 | public extractElement(array: T[], condition: (value: T) => boolean): [T | undefined, T[]] { 177 | return array.reduce<[T | undefined, T[]]>((accumulator, currentValue) => { 178 | if (accumulator[0] === undefined && condition(currentValue)) { 179 | accumulator[0] = currentValue 180 | } else { 181 | accumulator[1].push(currentValue) 182 | } 183 | return accumulator 184 | }, [undefined, []]) 185 | } 186 | 187 | public async minimize (id?: YabaiWindow['id'] | string): Promise { 188 | try { 189 | await $`yabai -m window ${id ?? ''} --minimize` 190 | } catch (error) { 191 | } 192 | } 193 | 194 | public async deminimize (id?: YabaiWindow['id'] | string): Promise { 195 | try { 196 | await $`yabai -m window ${id ?? ''} --deminimize` 197 | } catch (error) { 198 | } 199 | } 200 | 201 | public async above (id?: YabaiWindow['id'] | string): Promise { 202 | try { 203 | await $`yabai -m window ${id ?? ''} --layer above` 204 | } catch (error) { 205 | 206 | } 207 | } 208 | 209 | public async normal (id?: YabaiWindow['id'] | string): Promise { 210 | try { 211 | await $`yabai -m window ${id ?? ''} --layer normal` 212 | } catch (error) { 213 | } 214 | } 215 | 216 | public async move ({ 217 | x, y 218 | }: Pick, id?: YabaiWindow['id'] | string): Promise { 219 | try { 220 | const { stdout } = await $`yabai -m window ${id ?? ''} --move abs:${x}:${y}` 221 | return stdout 222 | } catch (error) { 223 | } 224 | } 225 | 226 | public async resize ({ 227 | w, h 228 | }: Pick, id?: YabaiWindow['id'] | string): Promise { 229 | try { 230 | const { stdout } = await $`yabai -m window ${id ?? ''} --resize abs:${w}:${h}` 231 | return stdout 232 | } catch (error) { 233 | } 234 | } 235 | 236 | public async focus ({ 237 | domain 238 | }: { 239 | domain: 'window' | 'space' | 'display' 240 | }, sel: number | string): Promise | undefined> { 241 | try { 242 | return await $`yabai -m ${domain} --focus ${sel}` 243 | } catch (error) { 244 | } 245 | } 246 | } 247 | 248 | export * from './yabai.types' 249 | --------------------------------------------------------------------------------