├── src-tauri ├── xrandr │ ├── .gitignore │ ├── .cargo_vcs_info.json │ ├── README.md │ ├── Cargo.toml.orig │ ├── Cargo.toml │ └── src │ │ ├── monitor.rs │ │ ├── screensize.rs │ │ ├── mode.rs │ │ ├── output │ │ └── mod.rs │ │ ├── screen_resources.rs │ │ ├── crtc.rs │ │ └── lib.rs ├── build.rs ├── icons │ ├── 32x32.png │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ ├── 128x128.png │ ├── 128x128@2.png │ ├── 128x128@2x.png │ ├── StoreLogo.png │ ├── Square30x30Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square310x310Logo.png │ ├── ios │ │ ├── AppIcon-512@2x.png │ │ ├── AppIcon-20x20@1x.png │ │ ├── AppIcon-20x20@2x.png │ │ ├── AppIcon-20x20@3x.png │ │ ├── AppIcon-29x29@1x.png │ │ ├── AppIcon-29x29@2x.png │ │ ├── AppIcon-29x29@3x.png │ │ ├── AppIcon-40x40@1x.png │ │ ├── AppIcon-40x40@2x.png │ │ ├── AppIcon-40x40@3x.png │ │ ├── AppIcon-60x60@2x.png │ │ ├── AppIcon-60x60@3x.png │ │ ├── AppIcon-76x76@1x.png │ │ ├── AppIcon-76x76@2x.png │ │ ├── AppIcon-20x20@2x-1.png │ │ ├── AppIcon-29x29@2x-1.png │ │ ├── AppIcon-40x40@2x-1.png │ │ └── AppIcon-83.5x83.5@2x.png │ └── android │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png │ │ └── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_round.png │ │ └── ic_launcher_foreground.png ├── .gitignore ├── src │ ├── main.rs │ └── lib.rs ├── capabilities │ └── default.json ├── Cargo.toml └── tauri.conf.json ├── src ├── vite-env.d.ts ├── main.tsx ├── components │ ├── LoadingScreen.tsx │ ├── Popups │ │ ├── SimplePopUp.tsx │ │ ├── SingleErrorPopUp.tsx │ │ ├── SingleErrorPopUp.css │ │ ├── MassApplyUndoPopup.css │ │ ├── SimplePopUp.css │ │ ├── ApplySettingsPopup.css │ │ ├── MassApplyUndoPopup.tsx │ │ └── ApplySettingsPopup.tsx │ ├── FreeHandPosition.css │ ├── Loading.css │ ├── Presets.css │ ├── Loaded.css │ ├── FocusedMonitorSettings.css │ ├── Presets.tsx │ ├── LoadedScreen.tsx │ ├── FocusedMonitorSettings.tsx │ └── FreeHandPosition.tsx ├── App.css ├── globalValues.tsx └── App.tsx ├── .vscode └── extensions.json ├── .prettierrc ├── tsconfig.node.json ├── .gitignore ├── index.html ├── tsconfig.json ├── package.json ├── vite.config.ts └── README.md /src-tauri/xrandr/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/128x128@2.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-512@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-20x20@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-20x20@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-29x29@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-29x29@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-40x40@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-40x40@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-60x60@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-60x60@3x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-76x76@1x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-76x76@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-20x20@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-20x20@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-29x29@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-29x29@2x-1.png -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-40x40@2x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-40x40@2x-1.png -------------------------------------------------------------------------------- /src-tauri/xrandr/.cargo_vcs_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "sha1": "cc06409aeace2076a73e0839ffce18bfb0809f8f" 4 | }, 5 | "path_in_vcs": "" 6 | } -------------------------------------------------------------------------------- /src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "jsxSingleQuote": true, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "arrowParens": "avoid" 8 | 9 | } -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bossadapt/Display-Settings-Plus/HEAD/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | ./Preset* -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | fn main() { 5 | display_settings_plus_lib::run() 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": ["main"], 6 | "permissions": [ 7 | "core:default", 8 | "opener:default" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/components/LoadingScreen.tsx: -------------------------------------------------------------------------------- 1 | // yoinked from https://codepen.io/AlbertFeynman/pen/zLEegX 2 | import './Loading.css'; 3 | function LoadingScreen() { 4 | return ( 5 |
6 |

LOADING

7 |
8 | ); 9 | } 10 | export default LoadingScreen; -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | 2 | body{ 3 | margin: 0%; 4 | } 5 | @media (prefers-color-scheme: dark) { 6 | :root { 7 | color: #f6f6f6; 8 | } 9 | 10 | a:hover { 11 | color: #24c8db; 12 | } 13 | 14 | input, 15 | button { 16 | color: #ffffff; 17 | background-color: black; 18 | } 19 | button:active { 20 | background-color: #0f0f0f69; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | src-tauri/Preset* 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tauri + React + Typescript 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src-tauri/xrandr/README.md: -------------------------------------------------------------------------------- 1 | # xrandr 2 | 3 | ![Crates.io](https://img.shields.io/crates/v/xrandr) 4 | ![Crates.io](https://img.shields.io/crates/l/xrandr) 5 | 6 | This crate aims to provide safe bindings to libxrandr, a library for 7 | communicating with monitors and displays using X11 on Linux. 8 | 9 | This crate currently supports reading most monitor properties. 10 | 11 | For the equivalent on Windows see [monitor-control-win][monitor-control-win-crate]. 12 | 13 | [monitor-control-win-crate]: https://crates.io/crates/monitor-control-win 14 | -------------------------------------------------------------------------------- /src/components/Popups/SimplePopUp.tsx: -------------------------------------------------------------------------------- 1 | import "./SimplePopUp.css"; 2 | 3 | interface SimplePopUpProps { 4 | showSimplePopUp: boolean, 5 | reasonForPopUp: string 6 | } 7 | export const SimplePopUp: React.FC = ({ showSimplePopUp, reasonForPopUp }) => { 8 | return ( 9 |
10 |
11 |
12 |

{reasonForPopUp}

13 |
14 |
15 | ); 16 | }; 17 | export default SimplePopUp; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "display-settings-plus", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri" 11 | }, 12 | "dependencies": { 13 | "@tauri-apps/api": "^2", 14 | "@tauri-apps/plugin-opener": "^2", 15 | "lodash": "^4.17.21", 16 | "pixi.js": "^8.8.0", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1", 19 | "react-select": "^5.9.0" 20 | }, 21 | "devDependencies": { 22 | "@tauri-apps/cli": "^2", 23 | "@types/lodash": "^4.14.195", 24 | "@types/react": "^18.3.1", 25 | "@types/react-dom": "^18.3.1", 26 | "@vitejs/plugin-react": "^4.3.4", 27 | "typescript": "~5.6.2", 28 | "vite": "^6.0.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src-tauri/xrandr/Cargo.toml.orig: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xrandr" 3 | version = "0.2.0" 4 | authors = [ "Rintse", "Daniel Franklin " ] 5 | edition = "2021" 6 | description = "Safe rust bindings to (some parts of) xrandr" 7 | license = "MIT" 8 | repository = "https://github.com/danielzfranklin/xrandr-rs" 9 | keywords = ["xrandr", "libxrandr", "libxrandr2", "bindings", "linux"] 10 | categories = ["os::linux-apis", "api-bindings"] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | thiserror = "1.0.24" 16 | x11 = { version = "2.18.2", features = ["xlib", "xrandr"] } 17 | indexmap = "1.6.2" 18 | serde = {version = "1.0.133", features=["derive"], optional=true} 19 | time = "0.3.20" 20 | itertools = "0.10.5" 21 | libc = "0.2.146" 22 | 23 | [features] 24 | serialize = ["serde", "indexmap/serde-1"] 25 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // @ts-expect-error process is a nodejs global 5 | const host = process.env.TAURI_DEV_HOST; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(async () => ({ 9 | plugins: [react()], 10 | 11 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 12 | // 13 | // 1. prevent vite from obscuring rust errors 14 | clearScreen: false, 15 | // 2. tauri expects a fixed port, fail if that port is not available 16 | server: { 17 | port: 1420, 18 | strictPort: true, 19 | host: host || false, 20 | hmr: host 21 | ? { 22 | protocol: "ws", 23 | host, 24 | port: 1421, 25 | } 26 | : undefined, 27 | watch: { 28 | // 3. tell vite to ignore watching `src-tauri` 29 | ignored: ["**/src-tauri/**"], 30 | }, 31 | }, 32 | })); 33 | -------------------------------------------------------------------------------- /src/components/FreeHandPosition.css: -------------------------------------------------------------------------------- 1 | .mini-titles{ 2 | width: 100%; 3 | text-align: center; 4 | margin-left: auto; 5 | margin-right: auto; 6 | font-size: 25px; 7 | align-content: center; 8 | } 9 | .right-freehand-container{ 10 | display: flex; 11 | flex-direction: column; 12 | height: 100%; 13 | width: 20vw; 14 | } 15 | .freehand-function-buttons{ 16 | width: 100%; 17 | height: 16%; 18 | padding:0px; 19 | } 20 | .scale-container{ 21 | height: 36%; 22 | width: 100%; 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: center; 26 | } 27 | .scale-container-mini{ 28 | display: flex; 29 | flex-direction: row; 30 | } 31 | .scale-base-text{ 32 | text-align: right; 33 | width: 50%; 34 | font-size: 20px; 35 | margin-top: auto; 36 | margin-bottom: auto; 37 | } 38 | .scale-input{ 39 | width: 50%; 40 | font-size: 20px; 41 | padding-top: 0px; 42 | margin-top: auto; 43 | margin-bottom: auto; 44 | } -------------------------------------------------------------------------------- /src/components/Popups/SingleErrorPopUp.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from "react"; 2 | import "./SingleErrorPopUp.css"; 3 | 4 | export interface SingleError { 5 | showSingleError: boolean, 6 | setShowSingleError: Dispatch>, 7 | singleErrorText: string 8 | setSingleErrorText: Dispatch> 9 | } 10 | export const SingleErrorPopup: React.FC = ({ showSingleError, singleErrorText, setShowSingleError }) => { 11 | return ( 12 |
13 |
14 |

Failed

15 |
16 |

{singleErrorText}

17 |
18 | 19 |
20 | ); 21 | }; 22 | export default SingleErrorPopup; -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "display-settings-plus" 3 | version = "0.1.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | edition = "2021" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "display_settings_plus_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2", features = [] } 19 | 20 | [dependencies] 21 | tauri = { version = "2", features = ["protocol-asset"] } 22 | tauri-plugin-opener = "2" 23 | xrandr = { version = "^0", path = "./xrandr" } 24 | serde = { workspace = true } 25 | serde_json = { workspace = true } 26 | tokio = { version ="1.43.0", features = ["macros"] } 27 | tokio-macros = "2.5.0" 28 | xcap = "0.3.3" 29 | directories = "6.0.0" 30 | lazy_static = "1.5.0" 31 | 32 | [workspace.dependencies] 33 | serde = { version = "1", features = ["derive"] } 34 | serde_json = "1" 35 | -------------------------------------------------------------------------------- /src/components/Loading.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Inconsolata'); 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | html, body { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | body { 13 | background: rgb(11, 11, 20); 14 | font-family: 'Inconsolata', monospace; 15 | overflow: hidden; 16 | overflow-y: scroll; 17 | margin: 0px; 18 | } 19 | 20 | .loading-h1 { 21 | position: absolute; 22 | height: 40px; 23 | margin: auto; 24 | top: 10px; 25 | left: 0; 26 | right: 0; 27 | bottom: 0; 28 | text-transform: uppercase; 29 | text-align: center; 30 | letter-spacing: 0.1em; 31 | font-size: 14px; 32 | font-weight: lighter; 33 | color: white; 34 | span { 35 | display: none; 36 | } 37 | &::after { 38 | animation: txt 5s infinite; 39 | content: ""; 40 | } 41 | } 42 | 43 | @keyframes cw { 44 | 0% { 45 | width: 0; 46 | height: 0; 47 | } 48 | 75% { 49 | width: 40px; 50 | height: 40px; 51 | } 52 | 100% { 53 | width: 0; 54 | height: 0; 55 | } 56 | } 57 | 58 | @keyframes txt { 59 | 0% { 60 | content: "LOADING."; 61 | } 62 | 50% { 63 | content: "LOADING.."; 64 | } 65 | 100% { 66 | content: "LOADING..."; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/components/Popups/SingleErrorPopUp.css: -------------------------------------------------------------------------------- 1 | .single-error-popup { 2 | display: block; 3 | position: fixed; 4 | padding: 10px; 5 | width: 400px; 6 | left: 50%; 7 | margin-left: -200px; 8 | height: 200px; 9 | top: 50%; 10 | margin-top: -225px; 11 | z-index: 20; 12 | } 13 | 14 | .single-error-popup:after { 15 | position: fixed; 16 | content: ""; 17 | top: 0; 18 | left: 0; 19 | bottom: 0; 20 | right: 0; 21 | background: rgba(0,0,0,0.5); 22 | z-index: -2; 23 | } 24 | 25 | .single-error-popup:before { 26 | position: absolute; 27 | content: ""; 28 | top: 0; 29 | left: 0; 30 | bottom: 0; 31 | right: 0; 32 | border-style: double; 33 | border-radius: 25px; 34 | border-color: hotpink; 35 | background-color: rgb(11, 11, 20); 36 | z-index: -1; 37 | } 38 | .single-error-contents{ 39 | display: flex; 40 | flex-direction: column; 41 | height: 135px; 42 | } 43 | .single-error-popup-title{ 44 | margin-left: auto; 45 | margin-right: auto; 46 | height: 40px; 47 | } 48 | .single-error-text{ 49 | margin-bottom: auto; 50 | } 51 | .single-error-accept-button{ 52 | margin: auto; 53 | width: 100%; 54 | border-radius: 0px 0px 20px 20px; 55 | } -------------------------------------------------------------------------------- /src/components/Popups/MassApplyUndoPopup.css: -------------------------------------------------------------------------------- 1 | .mass-apply-popup { 2 | display: block; 3 | position: fixed; 4 | padding: 10px; 5 | width: 300px; 6 | left: 50%; 7 | margin-left: -150px; 8 | height: 145px; 9 | top: 50%; 10 | margin-top: -100px; 11 | z-index: 20; 12 | } 13 | 14 | .mass-apply-popup:after { 15 | position: fixed; 16 | content: ""; 17 | top: 0; 18 | left: 0; 19 | bottom: 0; 20 | right: 0; 21 | background: rgba(0,0,0,0.5); 22 | z-index: -2; 23 | } 24 | 25 | .mass-apply-popup:before { 26 | position: absolute; 27 | content: ""; 28 | top: 0; 29 | left: 0; 30 | bottom: 0; 31 | right: 0; 32 | border-style: double; 33 | border-radius: 25px; 34 | border-color: hotpink; 35 | background-color: rgb(11, 11, 20); 36 | z-index: -1; 37 | } 38 | .mass-apply-contents{ 39 | display: flex; 40 | flex-direction: column; 41 | height: 135px; 42 | } 43 | .mass-apply-header{ 44 | display: flex; 45 | flex-direction: row; 46 | } 47 | .mass-apply-text{ 48 | margin-bottom: auto; 49 | } 50 | .mass-apply-undo-button{ 51 | margin: auto; 52 | width: 100%; 53 | height: 100%; 54 | color: hotpink; 55 | border-radius: 0px 0px 20px 20px; 56 | } 57 | .mass-apply-close-button{ 58 | margin: auto; 59 | width: 100%; 60 | height: 100%; 61 | color: red; 62 | border-radius: 20px 20px 0px 0px; 63 | margin-right: 0px; 64 | } -------------------------------------------------------------------------------- /src-tauri/xrandr/Cargo.toml: -------------------------------------------------------------------------------- 1 | # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO 2 | # 3 | # When uploading crates to the registry Cargo will automatically 4 | # "normalize" Cargo.toml files for maximal compatibility 5 | # with all versions of Cargo and also rewrite `path` dependencies 6 | # to registry (e.g., crates.io) dependencies. 7 | # 8 | # If you are reading this file be aware that the original Cargo.toml 9 | # will likely look very different (and much more reasonable). 10 | # See Cargo.toml.orig for the original contents. 11 | 12 | [package] 13 | edition = "2021" 14 | name = "xrandr" 15 | version = "0.2.0" 16 | authors = [ 17 | "Rintse", 18 | "Daniel Franklin ", 19 | ] 20 | description = "Safe rust bindings to (some parts of) xrandr" 21 | readme = "README.md" 22 | keywords = [ 23 | "xrandr", 24 | "libxrandr", 25 | "libxrandr2", 26 | "bindings", 27 | "linux", 28 | ] 29 | categories = [ 30 | "os::linux-apis", 31 | "api-bindings", 32 | ] 33 | license = "MIT" 34 | repository = "https://github.com/danielzfranklin/xrandr-rs" 35 | 36 | [dependencies.indexmap] 37 | version = "1.6.2" 38 | 39 | [dependencies.itertools] 40 | version = "0.10.5" 41 | 42 | [dependencies.libc] 43 | version = "0.2.146" 44 | 45 | 46 | [dependencies] 47 | serde = { workspace = true } 48 | serde_json = { workspace = true } 49 | 50 | [dependencies.thiserror] 51 | version = "1.0.24" 52 | 53 | [dependencies.time] 54 | version = "0.3.20" 55 | 56 | [dependencies.x11] 57 | version = "2.18.2" 58 | features = [ 59 | "xlib", 60 | "xrandr", 61 | ] 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/components/Presets.css: -------------------------------------------------------------------------------- 1 | .presets-top-container{ 2 | height: 75px; 3 | } 4 | .presets-list-container{ 5 | height: calc( 85% - 84px); 6 | overflow-y: scroll; 7 | } 8 | .presets-title{ 9 | width: 100%; 10 | height: 25px; 11 | text-align: center; 12 | margin-left: auto; 13 | margin-right: auto; 14 | margin-top: 5px; 15 | margin-bottom: 5px; 16 | } 17 | .presets-add-button{ 18 | height: 40px; 19 | width: 10px; 20 | } 21 | .presets-add-button:hover{ 22 | color:#39FF14; 23 | display: inline-flex; 24 | 25 | align-items: center; 26 | text-align: center; 27 | } 28 | .preset-delete-button{ 29 | width: 25%; 30 | } 31 | .preset-delete-button:hover{ 32 | color:red; 33 | } 34 | .selected-preset-button{ 35 | width: 75%; 36 | color:hotpink; 37 | } 38 | .unselected-preset-button{ 39 | width: 75%; 40 | } 41 | /*https://codepen.io/savwiley/pen/xxVRqXX*/ 42 | .presets-search-bar { 43 | width: 100%; 44 | padding: 10px; 45 | height: 40px; 46 | font-size: 18px; 47 | outline:none; 48 | background: linear-gradient(to left top, #000, #22132e) fixed; 49 | border: 2px solid rgba(255,255,255,0.2); 50 | color: rgba(255,255,255,0.8); 51 | transition: all 0.5s; 52 | } 53 | 54 | .presets-search-bar:hover { 55 | border: 2px solid rgba(255,255,255,0.5); 56 | } 57 | .presets-search-bar:focus { 58 | border: 2px solid rgba(255,255,255,0.5); 59 | background: linear-gradient(to left top, #000, #A64C79) fixed; 60 | } 61 | 62 | .overwrite-button{ 63 | width: 100%; 64 | height: 15%; 65 | padding: 0px; 66 | border: 0px; 67 | } -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "display-settings-plus", 4 | "version": "0.1.0", 5 | "identifier": "com.display-settings-plus.app", 6 | "build": { 7 | "beforeDevCommand": "npm run dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "npm run build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "windows": [ 14 | { 15 | "title": "display-settings-plus", 16 | "width": 800, 17 | "height": 600, 18 | "resizable": true 19 | } 20 | ], 21 | "security": { 22 | "csp": "default-src 'self' ipc: http://ipc.localhost asset://localhost; img-src 'self' asset: http://asset.localhost asset: asset://localhost data: blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self' 'unsafe-eval';connect-src 'self' asset://localhost;connect-src 'self' asset://localhost ipc://localhost;" 23 | , 24 | "assetProtocol": { 25 | "enable": true, 26 | "scope": ["$HOME/.config/display_settings_plus/screenshots/**"] 27 | } 28 | } 29 | }, 30 | "bundle": { 31 | "active": true, 32 | "targets": "all", 33 | "shortDescription": "GUI for Xrandr built with Rust's Tauri and React.ts", 34 | "longDescription": "A GUI for those who use X11's display server. This application allows you to edit the location, rotation, ratio and rate of your monitors.", 35 | "icon": [ 36 | "icons/32x32.png", 37 | "icons/128x128.png", 38 | "icons/128x128@2x.png", 39 | "icons/icon.icns", 40 | "icons/icon.ico" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Loaded.css: -------------------------------------------------------------------------------- 1 | 2 | .loadedMain { 3 | height: 100vh; 4 | display:flex; 5 | flex-direction: column; 6 | } 7 | .react-select{ 8 | height: 150px; 9 | } 10 | 11 | .majorButtons{ 12 | color: hotpink; 13 | font-size: 15px; 14 | height: 52px; 15 | border-color: white; 16 | padding-top: 8px; 17 | padding-bottom: 8px; 18 | border-bottom: 0px; 19 | } 20 | .majorButtons:hover{ 21 | border-bottom: 1px; 22 | } 23 | /* CSS from button people https://getcssscan.com/css-buttons-examples */ 24 | button { 25 | appearance: none; 26 | background-color: transparent; 27 | border: 1px solid #1A1A1A; 28 | 29 | box-sizing: border-box; 30 | color: #3B3B3B; 31 | cursor: pointer; 32 | display: inline-block; 33 | font-family: Roobert,-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; 34 | font-size: 16px; 35 | font-weight: 600; 36 | line-height: normal; 37 | margin: 0; 38 | min-height: 40px; 39 | min-width: 0; 40 | outline: none; 41 | padding: 16px 24px; 42 | text-align: center; 43 | text-decoration: none; 44 | transition: all 300ms cubic-bezier(.23, 1, 0.32, 1); 45 | user-select: none; 46 | -webkit-user-select: none; 47 | touch-action: manipulation; 48 | will-change: transform; 49 | } 50 | 51 | button:disabled { 52 | color: hotpink; 53 | pointer-events: none; 54 | } 55 | 56 | button:hover { 57 | color: hotpink; 58 | background-color: #1A1A1A; 59 | box-shadow: rgba(0, 0, 0, 0.25) 0 8px 15px; 60 | transform: translateY(-2px); 61 | } 62 | 63 | button:active { 64 | box-shadow: none; 65 | transform: translateY(0); 66 | } 67 | body{ 68 | background: rgb(11, 11, 20); 69 | } -------------------------------------------------------------------------------- /src/components/Popups/SimplePopUp.css: -------------------------------------------------------------------------------- 1 | .simplePopup { 2 | display: block; 3 | position: fixed; 4 | padding: 10px; 5 | width: 400px; 6 | left: 50%; 7 | margin-left: -200px; 8 | height: 450px; 9 | top: 50%; 10 | margin-top: -225px; 11 | 12 | z-index: 20; 13 | } 14 | 15 | .simplePopup:after { 16 | position: fixed; 17 | content: ""; 18 | top: 0; 19 | left: 0; 20 | bottom: 0; 21 | right: 0; 22 | background: rgba(0,0,0,0.5); 23 | z-index: -2; 24 | } 25 | 26 | .simplePopup:before { 27 | position: absolute; 28 | content: ""; 29 | top: 0; 30 | left: 0; 31 | bottom: 0; 32 | right: 0; 33 | z-index: -1; 34 | } 35 | .simplePopUpContents{ 36 | display:flex; 37 | height: 100%; 38 | width: 100%; 39 | flex-direction: column; 40 | } 41 | .simplePopUpText{ 42 | color: white; 43 | margin-left:auto; 44 | margin-bottom:auto; 45 | margin-right:auto; 46 | } 47 | /*https://css-loaders.com/spinner/ */ 48 | 49 | .simpleLoader { 50 | width: 50px; 51 | padding: 8px; 52 | margin-left:auto; 53 | margin-top:auto; 54 | margin-right:auto; 55 | aspect-ratio: 1; 56 | border-radius: 50%; 57 | background: hotpink; 58 | --_m: 59 | conic-gradient(#0000 10%,#000), 60 | linear-gradient(#000 0 0) content-box; 61 | -webkit-mask: var(--_m); 62 | mask: var(--_m); 63 | -webkit-mask-composite: source-out; 64 | mask-composite: subtract; 65 | animation: l3 1s infinite linear; 66 | } 67 | @keyframes l3 {to{transform: rotate(1turn)}} 68 | 69 | -------------------------------------------------------------------------------- /src-tauri/xrandr/src/monitor.rs: -------------------------------------------------------------------------------- 1 | use crate::output::Output; 2 | use crate::XHandle; 3 | use crate::XrandrError; 4 | use core::ptr; 5 | use std::slice; 6 | use x11::xrandr; 7 | 8 | // A wrapper that drops the pointer if it goes out of scope. 9 | // Avoid having to deal with the various early returns 10 | pub(crate) struct MonitorHandle { 11 | ptr: ptr::NonNull, 12 | count: i32, 13 | } 14 | 15 | impl MonitorHandle { 16 | pub(crate) fn new(handle: &mut XHandle) -> Result { 17 | let mut count = 0; 18 | 19 | let raw_ptr = 20 | unsafe { xrandr::XRRGetMonitors(handle.sys.as_ptr(), handle.root(), 0, &mut count) }; 21 | 22 | if count == -1 { 23 | return Err(XrandrError::GetMonitors); 24 | } 25 | 26 | let ptr = ptr::NonNull::new(raw_ptr).ok_or(XrandrError::GetMonitors)?; 27 | 28 | Ok(Self { ptr, count }) 29 | } 30 | 31 | pub(crate) fn as_slice(&self) -> &[xrandr::XRRMonitorInfo] { 32 | unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.count as usize) } 33 | } 34 | } 35 | 36 | impl Drop for MonitorHandle { 37 | fn drop(&mut self) { 38 | unsafe { xrandr::XRRFreeMonitors(self.ptr.as_ptr()) }; 39 | } 40 | } 41 | 42 | #[derive(Debug)] 43 | #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] 44 | pub struct Monitor { 45 | pub name: String, 46 | pub is_primary: bool, 47 | pub is_automatic: bool, 48 | pub x: i32, 49 | pub y: i32, 50 | pub width_px: i32, 51 | pub height_px: i32, 52 | pub width_mm: i32, 53 | pub height_mm: i32, 54 | /// An Output describes an actual physical monitor or display. A [`Monitor`] 55 | /// can have more than one output. 56 | pub outputs: Vec, 57 | } 58 | -------------------------------------------------------------------------------- /src/globalValues.tsx: -------------------------------------------------------------------------------- 1 | import { defaultTheme, Theme } from 'react-select'; 2 | 3 | export const customSelectTheme: Theme = { 4 | ...defaultTheme, 5 | borderRadius: 0, 6 | colors: { 7 | ...defaultTheme.colors, 8 | neutral0: 'black', 9 | neutral70: 'black', 10 | neutral80: 'white', 11 | //primary == background 12 | //hover over 13 | primary25: 'hotpink', 14 | primary50: 'pink', 15 | primary75: 'black', 16 | //already selected background from dropdown 17 | primary: 'hotpink', 18 | }, 19 | }; 20 | export interface Preset { 21 | name: string; 22 | monitors: FrontendMonitor[]; 23 | } 24 | export interface PositionProps { 25 | output_crtc?: number; 26 | x: String; 27 | y: String; 28 | } 29 | export interface point { 30 | x: number; 31 | y: number; 32 | } 33 | export enum Rotation { 34 | Normal = 'Normal', 35 | Left = 'Left', 36 | Inverted = 'Inverted', 37 | Right = 'Right', 38 | } 39 | export interface Mode { 40 | xid: number; 41 | width: number; 42 | height: number; 43 | dot_clock: number; 44 | hsync_tart: number; 45 | hsync_end: number; 46 | htotal: number; 47 | hskew: number; 48 | vsync_start: number; 49 | vsync_end: number; 50 | vtotal: number; 51 | name: String; 52 | flags: number; 53 | rate: number; 54 | } 55 | //These are more surface leveled versions of the xrandr versions 56 | export interface FrontendMonitor { 57 | name: string; 58 | imgSrc?: string; 59 | isPrimary: boolean; 60 | x: number; 61 | y: number; 62 | widthPx: number; 63 | heightPx: number; 64 | outputs: FrontendOutput[]; 65 | } 66 | 67 | export interface FrontendOutput { 68 | xid: number; 69 | isPrimary: boolean; 70 | enabled: boolean; 71 | crtc?: number; 72 | rotation: Rotation; 73 | name: string; 74 | connected: boolean; 75 | modes: Mode[]; 76 | preferredModes: Mode[]; 77 | currentMode: Mode; 78 | } 79 | 80 | export interface MiniMonitor { 81 | output_xid: number; 82 | enabled: boolean; 83 | rotation: Rotation; 84 | mode_xid: number; 85 | mode_height: number; 86 | mode_width: number; 87 | x: string; 88 | y: string; 89 | } 90 | -------------------------------------------------------------------------------- /src/components/Popups/ApplySettingsPopup.css: -------------------------------------------------------------------------------- 1 | .popup { 2 | display: block; 3 | position: fixed; 4 | padding: 10px; 5 | width: 400px; 6 | left: 50%; 7 | margin-left: -200px; 8 | height: 450px; 9 | top: 50%; 10 | margin-top: -225px; 11 | 12 | z-index: 20; 13 | } 14 | 15 | .popup:after { 16 | position: fixed; 17 | content: ""; 18 | top: 0; 19 | left: 0; 20 | bottom: 0; 21 | right: 0; 22 | background: rgba(0,0,0,0.5); 23 | z-index: -2; 24 | } 25 | 26 | .popup:before { 27 | position: absolute; 28 | content: ""; 29 | top: 0; 30 | left: 0; 31 | bottom: 0; 32 | right: 0; 33 | border-style: double; 34 | border-radius: 25px; 35 | border-color: hotpink; 36 | background-color: rgb(11, 11, 20); 37 | z-index: -1; 38 | } 39 | .popupTitle{ 40 | margin-left: auto; 41 | margin-right: auto; 42 | height: 40px; 43 | } 44 | .popupContentsContainer{ 45 | display: flex; 46 | flex-direction: column; 47 | height: 430px; 48 | } 49 | .monitorStatesContainer{ 50 | height: 410px; 51 | overflow-y: scroll; 52 | } 53 | /* Handle monitor state change*/ 54 | .monitorState{ 55 | margin-top: 10px; 56 | margin-right: auto; 57 | margin-left: auto; 58 | 59 | } 60 | .popupButtonContainer{ 61 | display: flex; 62 | flex-direction: row; 63 | width: 100%; 64 | } 65 | .errorTableContainer{ 66 | max-height: 100%; 67 | width: 100%; 68 | overflow-y: scroll; 69 | } 70 | .errorTable{ 71 | border-collapse: collapse; 72 | align-items: flex-start; 73 | } 74 | .errorTable > tbody > tr{ 75 | border-style: groove; 76 | border-top: 3px solid white; 77 | border-bottom: solid white; 78 | border-left: 0px; 79 | border-right: 0px; 80 | } 81 | 82 | .popupButton{ 83 | color:hotpink; 84 | width: 50%; 85 | height: 10px; 86 | } 87 | .finishButton{ 88 | width: 100%; 89 | margin-top: auto; 90 | height: 10px; 91 | border-radius: 0px 0px 10px 10px 92 | } 93 | #inProgress{ 94 | color: yellow; 95 | } 96 | #waiting{ 97 | color: grey; 98 | } 99 | 100 | #completed{ 101 | color: Green 102 | } 103 | #unchanged{ 104 | color: Green 105 | } 106 | #failed{ 107 | color: red 108 | } 109 | #undone{ 110 | color: cyan 111 | } 112 | -------------------------------------------------------------------------------- /src-tauri/xrandr/src/screensize.rs: -------------------------------------------------------------------------------- 1 | use crate::crtc::Crtc; 2 | use crate::XHandle; 3 | use x11::xlib; 4 | 5 | // The amount of milimeters in an inch, needed for dpi calculation 6 | const INCH_MM: f32 = 25.4; 7 | 8 | #[derive(Debug)] 9 | pub struct ScreenSize { 10 | pub(crate) width: i32, 11 | pub(crate) width_mm: i32, 12 | pub(crate) height: i32, 13 | pub(crate) height_mm: i32, 14 | } 15 | 16 | // Apparently this does not exist (in non-nightly)? 17 | // This function checks the requirements for a safe cast (right?), 18 | // so we allow possible trunction here and only here 19 | #[allow(clippy::cast_possible_truncation)] 20 | fn lossy_f32_to_i32(from: f32) -> Result { 21 | if from.round() >= i32::MIN as f32 && from.round() <= i32::MAX as f32 { 22 | Ok(from.round() as i32) 23 | } else { 24 | Err(()) 25 | } 26 | } 27 | 28 | impl ScreenSize { 29 | /// True iff the given crtc fits on a screen of this size 30 | #[must_use] 31 | pub fn fits_crtc(&self, crtc: &Crtc) -> bool { 32 | let (max_x, max_y) = crtc.max_coordinates(); 33 | //println!("maxX: {}, maxY:{}", max_x, max_y); 34 | //println!("width: {}, height:{}", self.width, self.height); 35 | max_x <= self.width && max_y <= self.height 36 | } 37 | 38 | /// Calculates the screen size that (snugly) fits a set of crtcs 39 | pub(crate) fn fitting_crtcs(handle: &mut XHandle, crtcs: &[Crtc]) -> Self { 40 | // see also: following unwraps 41 | //println!("crtc is empty: {}", crtcs.is_empty()); 42 | assert!(!crtcs.is_empty(), "Empty input vector"); 43 | 44 | let width = crtcs.iter().map(|p| p.max_coordinates().0).max().unwrap(); 45 | let height = crtcs.iter().map(|p| p.max_coordinates().1).max().unwrap(); 46 | 47 | // Get the old sizes to calculate the dpi 48 | let c_h = unsafe { xlib::XDisplayHeight(handle.sys.as_ptr(), 0) }; 49 | let c_h_mm = unsafe { xlib::XDisplayHeightMM(handle.sys.as_ptr(), 0) }; 50 | 51 | // Calculate the new physical size with the dpi and px count 52 | let dpi: f32 = (INCH_MM * c_h as f32) / c_h_mm as f32; 53 | 54 | // let x = (INCH_MM * width as f32) / dpi 55 | let width_mm = lossy_f32_to_i32((INCH_MM * width as f32) / dpi).unwrap(); 56 | let height_mm = lossy_f32_to_i32((INCH_MM * height as f32) / dpi).unwrap(); 57 | 58 | ScreenSize { 59 | width, 60 | width_mm, 61 | height, 62 | height_mm, 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src-tauri/xrandr/src/mode.rs: -------------------------------------------------------------------------------- 1 | use crate::XId; 2 | use serde::{Deserialize, Serialize}; 3 | use std::slice; 4 | use x11::xrandr; 5 | 6 | const RR_INTERLACE: u64 = 0x0000_0010; 7 | const RR_DOUBLE_SCAN: u64 = 0x0000_0020; 8 | 9 | // Modes correspond to the various display configurations the outputs 10 | // connected to your machine are capable of displaying. This mostly comes 11 | // down to resolution/refresh rates, but the `flags` field in particular 12 | // also encodes whether this mode is interlaced/doublescan 13 | //CUSTOM ADDED Serialize so that it works without reinitialization 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | pub struct Mode { 16 | pub xid: XId, 17 | pub width: u32, 18 | pub height: u32, 19 | pub dot_clock: u64, 20 | pub hsync_tart: u32, 21 | pub hsync_end: u32, 22 | pub htotal: u32, 23 | pub hskew: u32, 24 | pub vsync_start: u32, 25 | pub vsync_end: u32, 26 | pub vtotal: u32, 27 | pub name: String, 28 | pub flags: u64, 29 | pub rate: f64, 30 | } 31 | 32 | impl From<&xrandr::XRRModeInfo> for Mode { 33 | fn from(x_mode: &xrandr::XRRModeInfo) -> Self { 34 | let name_b = 35 | unsafe { slice::from_raw_parts(x_mode.name as *const u8, x_mode.nameLength as usize) }; 36 | 37 | // Calculate the refresh rate for this mode 38 | // This is not given by xrandr, but tends to be useful for end-users 39 | assert!( 40 | x_mode.hTotal != 0 && x_mode.vTotal != 0, 41 | "Framerate calculation would divide by zero" 42 | ); 43 | 44 | let v_total = if x_mode.modeFlags & RR_DOUBLE_SCAN != 0 { 45 | x_mode.vTotal * 2 46 | } else if x_mode.modeFlags & RR_INTERLACE != 0 { 47 | x_mode.vTotal / 2 48 | } else { 49 | x_mode.vTotal 50 | }; 51 | 52 | let rate = x_mode.dotClock as f64 / (f64::from(x_mode.hTotal) * f64::from(v_total)); 53 | 54 | Self { 55 | xid: x_mode.id, 56 | name: String::from_utf8_lossy(name_b).into_owned(), 57 | width: x_mode.width, 58 | height: x_mode.height, 59 | dot_clock: x_mode.dotClock, 60 | hsync_tart: x_mode.hSyncStart, 61 | hsync_end: x_mode.hSyncEnd, 62 | htotal: x_mode.hTotal, 63 | hskew: x_mode.hSkew, 64 | vsync_start: x_mode.vSyncStart, 65 | vsync_end: x_mode.vSyncEnd, 66 | vtotal: x_mode.vTotal, 67 | rate, 68 | flags: x_mode.modeFlags, 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/components/Popups/MassApplyUndoPopup.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, MutableRefObject, SetStateAction } from 'react'; 2 | import './MassApplyUndoPopup.css'; 3 | import { FrontendMonitor, MiniMonitor } from '../../globalValues'; 4 | import { invoke } from '@tauri-apps/api/core'; 5 | import { focusedSettingsFunctions } from '../LoadedScreen'; 6 | import { SingleError } from './SingleErrorPopUp'; 7 | import { cloneDeep } from 'lodash'; 8 | 9 | export interface MassApplyProps { 10 | showPopUp: boolean; 11 | initialMonitors: MutableRefObject; 12 | customMonitors: FrontendMonitor[]; 13 | setMonitors: Dispatch>; 14 | setShowMassUndoPopup: Dispatch>; 15 | toMiniMonitors: (monitors: FrontendMonitor[]) => MiniMonitor[]; 16 | singleErrorProps: SingleError; 17 | resetFunctions: MutableRefObject; 18 | setShowSimplePopup: Dispatch>; 19 | } 20 | export const MassApplyUndoPopup: React.FC = ({ 21 | showPopUp, 22 | initialMonitors, 23 | customMonitors, 24 | setMonitors, 25 | setShowMassUndoPopup, 26 | toMiniMonitors, 27 | singleErrorProps, 28 | resetFunctions, 29 | setShowSimplePopup, 30 | }) => { 31 | function closeHandle() { 32 | initialMonitors.current = [...customMonitors]; 33 | setShowMassUndoPopup(false); 34 | setShowSimplePopup(false); 35 | } 36 | async function undoHandle() { 37 | setShowSimplePopup(true); 38 | console.log(cloneDeep(initialMonitors.current)); 39 | let miniMonitors = toMiniMonitors(initialMonitors.current); 40 | await invoke<(number | undefined)[]>('quick_apply', { 41 | monitors: miniMonitors, 42 | }) 43 | .then(crtcs => { 44 | for (let i = 0; i < crtcs.length; i++) { 45 | if (crtcs[i]) { 46 | resetFunctions.current.setCrtc!(i, crtcs[i]!); 47 | } 48 | } 49 | }) 50 | .catch(err => { 51 | singleErrorProps.setShowSingleError(true); 52 | singleErrorProps.setSingleErrorText('Quick failed due to ' + err); 53 | }); 54 | setMonitors(cloneDeep(initialMonitors.current)); 55 | setShowMassUndoPopup(false); 56 | setShowSimplePopup(false); 57 | } 58 | return ( 59 |
60 |
61 | 69 |
70 | 78 |
79 |
80 | ); 81 | }; 82 | export default MassApplyUndoPopup; 83 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { invoke } from '@tauri-apps/api/core'; 3 | import { FrontendMonitor, Preset } from './globalValues'; 4 | import './App.css'; 5 | import LoadedScreen from './components/LoadedScreen'; 6 | import LoadingScreen from './components/LoadingScreen'; 7 | import { SingleError } from './components/Popups/SingleErrorPopUp'; 8 | function App() { 9 | const [customMonitorsInfo, setCustomMonitorsInfo] = useState([]); 10 | const didInit = useRef(false); 11 | const refreshMonitorsRef = useRef(refreshMonitors); 12 | const initialMonitorsInfo = useRef([]); 13 | const outputNames = useRef([]); 14 | const [presets, setPresets] = useState([]); 15 | const [showSingleError, setShowSingleError] = useState(false); 16 | const [singleErrorText, setSingleErrorText] = useState( 17 | 'Failed due to blah blah blahblahblahblahblahblah blah blah blah blah blah blah blah blah' 18 | ); 19 | const singleErrorProps: SingleError = { 20 | showSingleError, 21 | setShowSingleError, 22 | singleErrorText, 23 | setSingleErrorText, 24 | }; 25 | //-1 for cases where there are no monitors 26 | 27 | //grabbing monitors info every 5 seconds 28 | 29 | useEffect(() => { 30 | if (didInit.current) { 31 | return; 32 | } 33 | didInit.current = true; 34 | init(); 35 | }, []); 36 | 37 | //TODO: Update read me 38 | 39 | async function init() { 40 | await refreshMonitors(); 41 | await getPresets(); 42 | } 43 | // grabs monitors and updates screenshots 44 | async function refreshMonitors() { 45 | console.log('init called'); 46 | initialMonitorsInfo.current = []; 47 | setCustomMonitorsInfo([]); 48 | invoke<[FrontendMonitor[], String[]]>('get_monitors', {}) 49 | .then(res => { 50 | //res = handleRotations(res); 51 | outputNames.current = res[1]; 52 | console.log(res[1]); 53 | initialMonitorsInfo.current = [...res[0]]; 54 | setCustomMonitorsInfo(res[0]); 55 | console.log(res[0]); 56 | }) 57 | .catch(err => { 58 | singleErrorProps.setShowSingleError(true); 59 | singleErrorProps.setSingleErrorText('Monitor refresh/resync failed due to ' + err); 60 | }); 61 | } 62 | async function getPresets() { 63 | console.log('getPresets called'); 64 | invoke('get_presets', {}) 65 | .then(res => { 66 | setPresets(res); 67 | }) 68 | .catch(err => { 69 | singleErrorProps.setShowSingleError(true); 70 | singleErrorProps.setSingleErrorText('Getting presets failed due to ' + err); 71 | }); 72 | } 73 | return ( 74 |
75 | {customMonitorsInfo.length != 0 ? ( 76 | 86 | ) : ( 87 | 88 | )} 89 |
90 | ); 91 | } 92 | 93 | export default App; 94 | -------------------------------------------------------------------------------- /src/components/FocusedMonitorSettings.css: -------------------------------------------------------------------------------- 1 | .settingsList{ 2 | display: flex; 3 | flex-wrap: wrap; 4 | } 5 | .settingsContainer{ 6 | border-style: inset; 7 | border-width: 2px; 8 | border-radius: 20px; 9 | display: flex; 10 | display: row; 11 | border-color: hotpink; 12 | width: 96vw; 13 | margin-left: 2vw; 14 | margin-right: 2vw; 15 | margin-top: 2vh; 16 | margin-bottom: 2vh; 17 | height: 95px; 18 | } 19 | @media screen and (min-width: 1600px){ 20 | .settingsContainer{ 21 | width: 46vw; 22 | } 23 | } 24 | .mini-titles{ 25 | width: 100%; 26 | text-align: center; 27 | margin-left: auto; 28 | margin-right: auto; 29 | margin-top: 5px; 30 | margin-bottom: 5px; 31 | } 32 | .settingsDescriptonContainer{ 33 | width: 23%; 34 | margin-left: 5%; 35 | margin-right: 5%; 36 | margin-top: auto; 37 | margin-bottom: auto; 38 | 39 | } 40 | .settingsDescriptonContainer > h2{ 41 | margin:auto;; 42 | color: hotpink; 43 | } 44 | .settingsEditorContainer{ 45 | display: flex; 46 | display: row; 47 | width: calc(77% - 162px); 48 | } 49 | .resetButton{ 50 | height: 100%; 51 | margin-left: 10px; 52 | margin-right: 5px; 53 | margin-top: auto; 54 | margin-bottom: auto; 55 | width: 152px; 56 | border-radius: 0 20px 20px 0; 57 | 58 | } 59 | /*https://codepen.io/savwiley/pen/xxVRqXX*/ 60 | input[type="number"] { 61 | margin-left: 5px; 62 | margin-right: 5px; 63 | width: 100px; 64 | padding: 10px; 65 | height: 50px; 66 | font-size: 18px; 67 | outline:none; 68 | background: linear-gradient(to left top, #000, #22132e) fixed; 69 | border-radius: 10px; 70 | border: 2px solid rgba(255,255,255,0.2); 71 | color: rgba(255,255,255,0.8); 72 | transition: all 0.5s; 73 | } 74 | 75 | input[type="number"]:hover { 76 | border: 2px solid rgba(255,255,255,0.5); 77 | } 78 | input[type="number"]:focus { 79 | border: 2px solid rgba(255,255,255,0.5); 80 | background: linear-gradient(to left top, #000, #4e2d69) fixed; 81 | } 82 | @media screen and (min-width: 1000px){ 83 | input[type="number"] { 84 | width: 150px; 85 | height: 75 px; 86 | font-size: 25px; 87 | } 88 | } 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | /* */ 100 | 101 | .enable-container{ 102 | display: flex; 103 | width: 100%; 104 | height: 100%; 105 | justify-content: center; 106 | } 107 | input[type="checkbox"] { 108 | -webkit-appearance: none; 109 | -moz-appearance: none; 110 | appearance: none; 111 | -webkit-tap-highlight-color: transparent; 112 | cursor: pointer; 113 | } 114 | 115 | input[type="checkbox"]:focus { 116 | outline: 0; 117 | } 118 | 119 | .toggle { 120 | height: 50px; 121 | width: 150px; 122 | display: inline-block; 123 | position: relative; 124 | border: 2px solid #474755; 125 | background: linear-gradient(180deg, #2D2F39 0%, #1F2027 100%); 126 | transition: all 0.2s ease; 127 | margin: auto; 128 | } 129 | 130 | .toggle:after { 131 | content: ''; 132 | position: absolute; 133 | top: 2px; 134 | left: 2px; 135 | width: 70px; 136 | height: 42px; 137 | background: hotpink; 138 | box-shadow: 0 1px 2px rgba(44, 44, 44, 0.2); 139 | transition: all 0.2s cubic-bezier(0.5, 0.1, 0.75, 1.35); 140 | } 141 | 142 | input[type="checkbox"]:checked + .toggle { 143 | border-color: hotpink; 144 | } 145 | 146 | input[type="checkbox"]:checked + .toggle:after { 147 | transform: translateX(72px); 148 | } 149 | 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Display Settings Plus 2 | 3 | **Display Settings Plus** is a GUI for [Xrandr](https://www.x.org/archive/X11R7.6/doc/man/man3/Xrandr.3.xhtml) that allows users to rotate, resize, position, and apply presets to their monitors. The changes are performed using a modified version of a [Rust Xrandr wrapper](https://github.com/dzfranklin/xrandr-rs), with abstraction provided by Tauri and React TypeScript. 4 | ![Screenshot of the application](https://github.com/user-attachments/assets/d68ccadd-d986-40d5-ae53-46058bd0c9a7) 5 | ## How to use 6 | 1. Simply choose and download which bundle you favor from the latest release from the releases tab 7 | 2. Install using your favorite package manager 8 | 3. Thaaats it, Nearly all buttons have descriptions of their effects on the application. 9 | 10 | However, if you need further explanations beyond the buttons' whispers, an explanation of every button is found on my [blog](https://bossadapt.org/blogs/posts/how-to-use-display-settings-plus/) 11 | ## Display Settings Plus vs. Distro Display Settings 12 | 13 | | Functionality | Display Settings Plus | Ubuntu Display Settings | 14 | |--------------|----------------------|-----------------------| 15 | | Undo | Modular[Beta] & Whole | Whole | 16 | | Disable | :white_check_mark: | :white_check_mark: | 17 | | Screenshot | :white_check_mark: | :x: | 18 | | Position Panning | :white_check_mark: | :x: | 19 | | Position Zooming | Manual Dynamic | Auto | 20 | | Position Gaps Between Monitors | :white_check_mark: | :x: | 21 | | Position Snapping | 8-point snapping (corners and edge centers align) | Hugs border | 22 | | Monitor Mirroring (Overlapping) | All, Individual, or None | All or None | 23 | | Rotation | :white_check_mark: | :white_check_mark: | 24 | | Resolution | :white_check_mark: | :white_check_mark: | 25 | | Refresh Rate | :white_check_mark: | :white_check_mark: | 26 | | Scale | :x: | :white_check_mark: | 27 | | Night Mode | :x: | :white_check_mark: | 28 | | Export | JSON + Copies Xrandr scripts to clipboard | :x: | 29 | | Presets | :white_check_mark: | :x: | 30 | | Permanent | Requires exporting to a location where X11 runs on boot | :white_check_mark: | 31 | | Language | English | Multi | 32 | 33 | ## Display Settings Plus vs. [ARandR](https://github.com/haad/arandr) 34 | 35 | | Functionality | Display Settings Plus | ARandR | 36 | |--------------|----------------------|--------| 37 | | Undo | Modular[Beta] & Whole | :x: | 38 | | Disable | :white_check_mark: | :white_check_mark: | 39 | | Screenshot | :white_check_mark: | :x: | 40 | | Position Panning | :white_check_mark: | :x: | 41 | | Position Zooming | Manual Dynamic | 3 options (1:4, 8, 16) | 42 | | Position Gaps Between Monitors | :white_check_mark: | :white_check_mark: | 43 | | Position Snapping | 8-point snapping (corners and edge centers align) | Hugs border | 44 | | Monitor Mirroring (Overlapping) | All, Individual, or None | All, Individual, or None | 45 | | Rotation | :white_check_mark: | :white_check_mark: | 46 | | Resolution | :white_check_mark: | :white_check_mark: | 47 | | Refresh Rate | :white_check_mark: | :x: | 48 | | Scale | :x: | :x: | 49 | | Night Mode | :x: | :x: | 50 | | Export | JSON + Copies Xrandr scripts to clipboard | `.sh` Files | 51 | | Presets | :white_check_mark: | Opens a `.sh` file | 52 | | Permanent | Requires exporting to a location where X11 runs on boot | Same, but you can move the entire `.sh` file or extract the script from it | 53 | | Language | English | Multi | 54 | 55 | ## How Does It Work? 56 | In short, It balances the state in the frontend with 2 states custom(state of all customization user makes without applying) and initial(state of last applied/ pulled from the system) 57 | ## Future Plans 58 | Possibly expanding usage to other display manager servers or more focused monitor settings. 59 | -------------------------------------------------------------------------------- /src-tauri/xrandr/src/output/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::screen_resources::ScreenResourcesHandle; 2 | use crate::{Crtc, ScreenResources, XHandle, XrandrError}; 3 | use std::os::raw::c_int; 4 | use std::{ptr, slice}; 5 | use x11::xrandr; 6 | 7 | use crate::XId; 8 | use crate::XTime; 9 | use crate::CURRENT_TIME; 10 | 11 | #[derive(Debug)] 12 | #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] 13 | pub struct Output { 14 | pub xid: XId, 15 | pub timestamp: XTime, 16 | pub is_primary: bool, 17 | pub crtc: Option, 18 | pub name: String, 19 | pub mm_width: u64, 20 | pub mm_height: u64, 21 | pub connected: bool, 22 | pub subpixel_order: u16, 23 | pub crtcs: Vec, 24 | pub clones: Vec, 25 | pub modes: Vec, 26 | pub preferred_modes: Vec, 27 | pub current_mode: Option, 28 | } 29 | 30 | // A wrapper that drops the pointer if it goes out of scope. 31 | // Avoid having to deal with the various early returns 32 | struct OutputHandle { 33 | ptr: ptr::NonNull, 34 | } 35 | 36 | impl OutputHandle { 37 | fn new(handle: &mut XHandle, xid: XId) -> Result { 38 | let res = ScreenResourcesHandle::new(handle)?; 39 | 40 | let raw_ptr = unsafe { xrandr::XRRGetOutputInfo(handle.sys.as_ptr(), res.ptr(), xid) }; 41 | 42 | let ptr = ptr::NonNull::new(raw_ptr).ok_or(XrandrError::GetOutputInfo(xid))?; 43 | 44 | Ok(Self { ptr }) 45 | } 46 | } 47 | 48 | impl Drop for OutputHandle { 49 | fn drop(&mut self) { 50 | unsafe { xrandr::XRRFreeOutputInfo(self.ptr.as_ptr()) }; 51 | } 52 | } 53 | 54 | impl Output { 55 | /// Get the Output's EDID property, if it exists. 56 | /// 57 | /// EDID stands for Extended Device Identification Data. You can parse it 58 | /// with a crate such as [edid][edid-crate] to get information such as the 59 | /// device model or colorspace. 60 | /// 61 | /// [edid-crate]: https://crates.io/crates/edid 62 | // #[must_use] 63 | // pub fn edid(&self) -> Option> { 64 | // self.properties.get("EDID").map(|prop| match &prop.value { 65 | // Value::Edid(edid) => edid.clone(), 66 | // _ => unreachable!("Property with name EDID should have type edid"), 67 | // }) 68 | // } 69 | 70 | pub(crate) fn from_xid( 71 | handle: &mut XHandle, 72 | xid: u64, 73 | crtcs_vec: Option<&Vec>, 74 | res: &ScreenResources, 75 | ) -> Result { 76 | let output_info = OutputHandle::new(handle, xid)?; 77 | 78 | let xrandr::XRROutputInfo { 79 | crtc, 80 | ncrtc, 81 | crtcs, 82 | nmode, 83 | npreferred, 84 | modes, 85 | name, 86 | nameLen, 87 | connection, 88 | mm_width, 89 | mm_height, 90 | subpixel_order, 91 | .. 92 | } = unsafe { output_info.ptr.as_ref() }; 93 | let connected = c_int::from(*connection) == xrandr::RR_Connected; 94 | // Name processing 95 | let name_b = unsafe { slice::from_raw_parts(*name as *const u8, *nameLen as usize) }; 96 | 97 | let name = String::from_utf8_lossy(name_b).to_string(); 98 | // let properties = Self::get_props(handle, xid)?; 99 | //There is no reason to pull information about monitors that are not connected 100 | if connected { 101 | let is_primary = 102 | xid == unsafe { xrandr::XRRGetOutputPrimary(handle.sys.as_ptr(), handle.root()) }; 103 | 104 | // let clones = unsafe { 105 | // slice::from_raw_parts(*clones, *nclone as usize) }; 106 | 107 | let modes = unsafe { slice::from_raw_parts(*modes, *nmode as usize) }; 108 | 109 | let preferred_modes = modes[0..*npreferred as usize].to_vec(); 110 | 111 | let crtcs = unsafe { slice::from_raw_parts(*crtcs, *ncrtc as usize) }; 112 | 113 | let crtc_id = if *crtc == 0 { None } else { Some(*crtc) }; 114 | let curr_crtc: Option; 115 | if let Some(crtcs_vec) = crtcs_vec { 116 | curr_crtc = match crtc_id { 117 | Some(crtc_id) => Some( 118 | crtcs_vec 119 | .iter() 120 | .find(|crtc| crtc.xid == crtc_id) 121 | .unwrap() 122 | .clone(), 123 | ), 124 | _ => None, 125 | }; 126 | } else { 127 | curr_crtc = res.crtc(handle, xid).ok(); 128 | } 129 | 130 | let current_mode = curr_crtc 131 | .and_then(|crtc_info| modes.iter().copied().find(|&m| m == crtc_info.mode)); 132 | 133 | let result = Self { 134 | xid, 135 | timestamp: CURRENT_TIME, 136 | is_primary, 137 | crtc: crtc_id, 138 | name, 139 | mm_width: *mm_width, 140 | mm_height: *mm_height, 141 | connected, 142 | subpixel_order: *subpixel_order, 143 | crtcs: crtcs.to_vec(), 144 | clones: Default::default(), 145 | modes: modes.to_vec(), 146 | preferred_modes, 147 | current_mode, 148 | }; 149 | 150 | Ok(result) 151 | } else { 152 | let result = Self { 153 | xid, 154 | timestamp: CURRENT_TIME, 155 | is_primary: false, 156 | crtc: Default::default(), 157 | name, 158 | mm_width: *mm_width, 159 | mm_height: *mm_height, 160 | connected, 161 | subpixel_order: Default::default(), 162 | crtcs: Default::default(), 163 | clones: Default::default(), 164 | modes: Default::default(), 165 | preferred_modes: Default::default(), 166 | current_mode: None, 167 | }; 168 | 169 | Ok(result) 170 | } 171 | } 172 | 173 | pub(crate) unsafe fn from_list( 174 | handle: &mut XHandle, 175 | data: *mut xrandr::RROutput, 176 | len: c_int, 177 | res: &ScreenResources, 178 | ) -> Result, XrandrError> { 179 | slice::from_raw_parts(data, len as usize) 180 | .iter() 181 | .map(|xid| Output::from_xid(handle, *xid, None, res)) 182 | .collect() 183 | } 184 | } 185 | 186 | // #[cfg(test)] 187 | // mod tests { 188 | // use crate::XHandle; 189 | 190 | // #[test] 191 | // fn can_get_output_edid() { 192 | // let outputs = XHandle::open().unwrap().all_outputs().unwrap(); 193 | // let output = outputs.first().unwrap(); 194 | // let edid = output.edid().unwrap(); 195 | // println!("{:?}", edid); 196 | // } 197 | // } 198 | -------------------------------------------------------------------------------- /src/components/Presets.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, MutableRefObject, SetStateAction, useState } from 'react'; 2 | import { FrontendMonitor, Preset } from '../globalValues'; 3 | import { invoke } from '@tauri-apps/api/core'; 4 | import './Presets.css'; 5 | import { SingleError } from './Popups/SingleErrorPopUp'; 6 | interface PresetsProps { 7 | presets: Preset[]; 8 | setPresets: Dispatch>; 9 | customMonitors: FrontendMonitor[]; 10 | setCustMonitors: Dispatch>; 11 | normalizePositionsRef: MutableRefObject< 12 | ((customMonitors: FrontendMonitor[]) => FrontendMonitor[]) | null 13 | >; 14 | setShowSimplePopUp: Dispatch>; 15 | setSimplePopUpReason: Dispatch>; 16 | singleError: SingleError; 17 | } 18 | export const Presets: React.FC = ({ 19 | presets, 20 | setPresets, 21 | customMonitors, 22 | setCustMonitors, 23 | normalizePositionsRef, 24 | setShowSimplePopUp, 25 | setSimplePopUpReason, 26 | singleError, 27 | }) => { 28 | const [focusedPresetValue, setFocusedPresetValue] = useState(undefined); 29 | const [presetSearchTerm, setPresetSearchTerm] = useState(''); 30 | function setFocusedPreset(preset: Preset) { 31 | let newMons: FrontendMonitor[] = []; 32 | for (let i = 0; i < customMonitors.length; i++) { 33 | // has the same xid and has the mode xid needed available 34 | let presetAttempt = preset.monitors.find( 35 | presetMon => 36 | customMonitors[i].outputs[0].xid === presetMon.outputs[0].xid && 37 | customMonitors[i].outputs[0].modes.find( 38 | mode => mode.xid === presetMon.outputs[0].currentMode.xid 39 | ) 40 | ); 41 | if (presetAttempt) { 42 | console.log('monitor number ', i, ' was overwritten'); 43 | } 44 | newMons.push(presetAttempt ? { ...presetAttempt } : customMonitors[i]); 45 | } 46 | setCustMonitors(newMons); 47 | setFocusedPresetValue(preset); 48 | } 49 | function overwriteFocusedPreset() { 50 | if (focusedPresetValue && normalizePositionsRef.current) { 51 | let normalizedMonitors = normalizePositionsRef.current(customMonitors); 52 | let newMonitors = normalizedMonitors.map(mon => ({ 53 | ...mon, 54 | x: Number(mon.x.toFixed(0)), 55 | y: Number(mon.y.toFixed(0)), 56 | })); 57 | let newPreset = { name: focusedPresetValue.name, monitors: newMonitors }; 58 | setShowSimplePopUp(true); 59 | setSimplePopUpReason('Overwriting Preset'); 60 | invoke('create_preset', { 61 | preset: { name: focusedPresetValue.name, monitors: newMonitors }, 62 | }) 63 | .then(_res => { 64 | setPresets(oldPresets => 65 | oldPresets.map(preset => (preset.name == focusedPresetValue.name ? newPreset : preset)) 66 | ); 67 | setFocusedPresetValue(newPreset); 68 | }) 69 | .catch(err => { 70 | singleError.setShowSingleError(true); 71 | singleError.setSingleErrorText('Overwrite Preset failed due to ' + err); 72 | }); 73 | setShowSimplePopUp(false); 74 | } 75 | } 76 | function deletePreset(presetName: string) { 77 | setShowSimplePopUp(true); 78 | setSimplePopUpReason('Deleting Preset'); 79 | console.log('deleting ', presetName); 80 | invoke('delete_preset', { presetName }) 81 | .then(_res => { 82 | if (focusedPresetValue && focusedPresetValue.name == presetName) { 83 | setFocusedPresetValue(undefined); 84 | } 85 | setPresets(oldPresets => oldPresets.filter(preset => preset.name != presetName)); 86 | console.log('preset deleted'); 87 | }) 88 | .catch(err => { 89 | singleError.setShowSingleError(true); 90 | singleError.setSingleErrorText('Delete failed due to ' + err); 91 | }); 92 | setShowSimplePopUp(false); 93 | } 94 | function createPreset() { 95 | if (presetSearchTerm.trim() !== '') { 96 | let preset = { name: presetSearchTerm, monitors: [] }; 97 | setShowSimplePopUp(true); 98 | setSimplePopUpReason('Creating Preset'); 99 | invoke('create_preset', { 100 | preset, 101 | }) 102 | .then(_res => { 103 | if (presets.findIndex(preset => preset.name == presetSearchTerm) !== -1) { 104 | setPresets(oldPresets => 105 | oldPresets.map(preset => 106 | preset.name == presetSearchTerm ? { name: preset.name, monitors: [] } : preset 107 | ) 108 | ); 109 | } else { 110 | setPresets(oldPresets => { 111 | let newPresets = [...oldPresets]; 112 | newPresets.push(preset); 113 | return newPresets; 114 | }); 115 | } 116 | setPresetSearchTerm(''); 117 | }) 118 | .catch(err => { 119 | singleError.setShowSingleError(true); 120 | singleError.setSingleErrorText('Create failed due to ' + err); 121 | }); 122 | setShowSimplePopUp(false); 123 | } 124 | } 125 | return ( 126 |
127 |
128 |

Presets

129 |
130 | { 136 | setPresetSearchTerm(eve.target.value); 137 | }} 138 | /> 139 | 142 |
143 |
144 |
145 |
146 | {presets 147 | .filter(preset => preset.name.includes(presetSearchTerm)) 148 | .sort((a, b) => (a.name > b.name ? 1 : -1)) 149 | .map(preset => ( 150 |
151 | 162 | 169 |
170 | ))} 171 |
172 |
173 | 180 |
181 | ); 182 | }; 183 | export default Presets; 184 | -------------------------------------------------------------------------------- /src-tauri/xrandr/src/screen_resources.rs: -------------------------------------------------------------------------------- 1 | use std::{ptr, slice}; 2 | use x11::xrandr; 3 | 4 | use crate::crtc::Crtc; 5 | use crate::output::Output; 6 | use crate::Mode; 7 | use crate::XHandle; 8 | use crate::XrandrError; 9 | 10 | use crate::XId; 11 | use crate::XTime; 12 | 13 | // A wrapper that drops the pointer if it goes out of scope. 14 | // Avoid having to deal with the various early returns 15 | pub(crate) struct ScreenResourcesHandle { 16 | ptr: ptr::NonNull, 17 | } 18 | 19 | impl ScreenResourcesHandle { 20 | pub(crate) fn new(handle: &mut XHandle) -> Result { 21 | let raw_ptr = unsafe { xrandr::XRRGetScreenResources(handle.sys.as_ptr(), handle.root()) }; 22 | 23 | let ptr = ptr::NonNull::new(raw_ptr).ok_or(XrandrError::GetResources)?; 24 | Ok(Self { ptr }) 25 | } 26 | 27 | pub(crate) fn ptr(&self) -> *mut x11::xrandr::XRRScreenResources { 28 | self.ptr.as_ptr() 29 | } 30 | } 31 | 32 | impl Drop for ScreenResourcesHandle { 33 | fn drop(&mut self) { 34 | unsafe { xrandr::XRRFreeScreenResources(self.ptr.as_ptr()) }; 35 | } 36 | } 37 | 38 | #[derive(Debug)] 39 | pub struct ScreenResources { 40 | pub timestamp: XTime, 41 | pub config_timestamp: XTime, 42 | pub ncrtc: i32, 43 | crtcs: Vec, 44 | pub outputs: Vec, 45 | pub nmode: i32, 46 | pub modes: Vec, 47 | } 48 | 49 | impl ScreenResources { 50 | /// Create a handle to the `XRRScreenResources` object from libxrandr. 51 | /// This handle is used to query many parts of the current x11 config. 52 | /// 53 | /// # Errors 54 | /// * `XrandrError::GetResources` - Getting the handle failed. 55 | /// 56 | /// # Examples 57 | /// ``` 58 | /// let xhandle = XHandle.open()?; 59 | /// let res = ScreenResources::new(&mut xhandle)?; 60 | /// let crtc_87 = res.crtc(&mut xhandle, 87); 61 | /// ``` 62 | /// 63 | pub fn new(handle: &mut XHandle) -> Result { 64 | // TODO: does this need to be freed? 65 | let res = ScreenResourcesHandle::new(handle)?; 66 | let xrandr::XRRScreenResources { 67 | modes, 68 | nmode, 69 | crtcs, 70 | ncrtc, 71 | outputs, 72 | noutput, 73 | timestamp, 74 | configTimestamp, 75 | .. 76 | } = unsafe { res.ptr.as_ref() }; 77 | 78 | let x_modes: &[xrandr::XRRModeInfo] = 79 | unsafe { slice::from_raw_parts(*modes, *nmode as usize) }; 80 | 81 | let modes: Vec = x_modes.iter().map(Mode::from).collect(); 82 | 83 | let x_crtcs = unsafe { slice::from_raw_parts(*crtcs, *ncrtc as usize) }; 84 | 85 | let x_outputs = unsafe { slice::from_raw_parts(*outputs, *noutput as usize) }; 86 | 87 | Ok(ScreenResources { 88 | timestamp: *timestamp, 89 | config_timestamp: *configTimestamp, 90 | ncrtc: *ncrtc, 91 | crtcs: x_crtcs.to_vec(), 92 | outputs: x_outputs.to_vec(), 93 | nmode: *nmode, 94 | modes, 95 | }) 96 | } 97 | 98 | /// Gets information on all outputs 99 | /// 100 | /// # Errors 101 | /// * `XrandrError::GetOutputInfo(xid)` 102 | /// -- Getting info failed for output xid 103 | /// 104 | /// # Examples 105 | /// ``` 106 | /// let res = ScreenResources::new(&mut xhandle)?; 107 | /// let outputs = res.outputs(&mut xhandle); 108 | /// ``` 109 | /// 110 | pub fn outputs( 111 | &self, 112 | handle: &mut XHandle, 113 | crtcs: Option<&Vec>, 114 | res: &ScreenResources, 115 | ) -> Result, XrandrError> { 116 | self.outputs 117 | .iter() 118 | .map(|xid| Output::from_xid(handle, *xid, crtcs, res)) 119 | .collect() 120 | } 121 | 122 | /// Gets information on output with given xid 123 | /// 124 | /// # Errors 125 | /// * `XrandrError::GetOutputInfo(xid)` 126 | /// -- Getting info failed for output with XID `xid` 127 | /// 128 | /// # Examples 129 | /// ``` 130 | /// let res = ScreenResources::new(&mut xhandle)?; 131 | /// let output_89 = res.output(&mut xhandle, 89); 132 | /// ``` 133 | /// 134 | pub fn output( 135 | &self, 136 | handle: &mut XHandle, 137 | xid: XId, 138 | crtcs: Option<&Vec>, 139 | res: &ScreenResources, 140 | ) -> Result { 141 | Output::from_xid(handle, xid, crtcs, res) 142 | } 143 | 144 | /// Gets information on all crtcs 145 | /// 146 | /// # Errors 147 | /// * `XrandrError::GetCrtcInfo(xid)` 148 | /// -- Getting info failed for crtc with XID `xid` 149 | /// 150 | /// # Examples 151 | /// ``` 152 | /// let res = ScreenResources::new(&mut xhandle)?; 153 | /// let crtcs = res.crtcs(&mut xhandle); 154 | /// ``` 155 | /// 156 | pub fn crtcs(&self, handle: &mut XHandle) -> Result, XrandrError> { 157 | self.crtcs 158 | .iter() 159 | .map(|xid| Crtc::from_xid(handle, *xid)) 160 | .collect() 161 | } 162 | 163 | /// Gets information of only the enabled crtcs 164 | /// See also: `self.crtcs()` 165 | /// # Errors 166 | /// * `XrandrError::GetCrtcInfo(xid)` 167 | /// -- Getting info failed for crtc with XID `xid` 168 | /// 169 | pub fn enabled_crtcs(&self, handle: &mut XHandle) -> Result, XrandrError> { 170 | Ok(self 171 | .crtcs(handle)? 172 | .into_iter() 173 | .filter(|c| c.mode != 0) 174 | .collect()) 175 | } 176 | 177 | /// Gets information on crtc with given xid 178 | /// 179 | /// # Errors 180 | /// * `XrandrError::GetCrtcInfo(xid)` 181 | /// -- Getting info failed for crtc with XID `xid` 182 | /// 183 | /// # Examples 184 | /// ``` 185 | /// let res = ScreenResources::new(&mut xhandle)?; 186 | /// let current_crtc = res.crtc(&mut xhandle, output.crtc); 187 | /// ``` 188 | /// 189 | pub fn crtc(&self, handle: &mut XHandle, xid: XId) -> Result { 190 | Crtc::from_xid(handle, xid) 191 | } 192 | 193 | /// Gets information on all crtcs 194 | /// 195 | /// # Errors 196 | /// * `XrandrError::GetCrtcInfo(xid)` 197 | /// -- Getting info failed for crtc with XID `xid` 198 | /// 199 | /// # Examples 200 | /// ``` 201 | /// let res = ScreenResources::new(&mut xhandle)?; 202 | /// let crtcs = res.crtcs(&mut xhandle); 203 | /// ``` 204 | /// 205 | #[must_use] 206 | pub fn modes(&self) -> Vec { 207 | self.modes.clone() 208 | } 209 | 210 | /// Gets information on mode with given xid 211 | /// 212 | /// # Errors 213 | /// * `XrandrError::GetModeInfo(xid)` 214 | /// -- Getting info failed for mode with XID `xid` 215 | /// 216 | /// # Examples 217 | /// ``` 218 | /// let res = ScreenResources::new(&mut xhandle)?; 219 | /// let current_mode = res.mode(&mut xhandle, output.mode); 220 | /// ``` 221 | /// 222 | pub fn mode(&self, xid: XId) -> Result { 223 | self.modes 224 | .iter() 225 | .find(|c| c.xid == xid) 226 | .cloned() 227 | .ok_or(XrandrError::GetModeInfo(xid)) 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src-tauri/xrandr/src/crtc.rs: -------------------------------------------------------------------------------- 1 | use crate::screen_resources::ScreenResourcesHandle; 2 | use crate::XHandle; 3 | use crate::XId; 4 | use crate::XTime; 5 | use crate::XrandrError; 6 | use crate::CURRENT_TIME; 7 | use std::ptr; 8 | use std::slice; 9 | 10 | use serde::Deserialize; 11 | use serde::Serialize; 12 | use std::convert::TryFrom; 13 | use x11::xrandr; 14 | 15 | // A Crtc can display a mode in one of 4 rotations 16 | #[derive(Serialize, Deserialize, PartialEq, Eq, Copy, Debug, Clone)] 17 | pub enum Rotation { 18 | Normal = 1, 19 | Left = 2, 20 | Inverted = 4, 21 | Right = 8, 22 | } 23 | 24 | impl TryFrom for Rotation { 25 | type Error = XrandrError; 26 | 27 | fn try_from(r: u16) -> Result { 28 | match r { 29 | 1 => Ok(Rotation::Normal), 30 | 2 => Ok(Rotation::Left), 31 | 4 => Ok(Rotation::Inverted), 32 | 8 => Ok(Rotation::Right), 33 | _ => Err(XrandrError::InvalidRotation(r)), 34 | } 35 | } 36 | } 37 | 38 | // A Crtc can be positioned relative to another one in one of five directions 39 | #[derive(Copy, Debug, Clone)] 40 | pub enum Relation { 41 | LeftOf, 42 | RightOf, 43 | Above, 44 | Below, 45 | SameAs, 46 | } 47 | 48 | // Crtcs define a region of pixels you can see. The Crtc controls the size 49 | // and timing of the signal. To this end, the Crtc struct in xrandr maintains 50 | // a list of attributes that usually correspond to a physical display. 51 | #[derive(PartialEq, Eq, Debug, Clone)] 52 | pub struct Crtc { 53 | pub xid: XId, 54 | pub timestamp: XTime, 55 | pub x: i32, 56 | pub y: i32, 57 | pub width: u32, 58 | pub height: u32, 59 | pub mode: XId, 60 | pub rotation: Rotation, 61 | pub outputs: Vec, 62 | pub rotations: u16, 63 | pub possible: Vec, 64 | } 65 | 66 | /// Normalizes a set of Crtcs by making sure the top left pixel of the screen 67 | /// is at (0,0). This is needed after changing positions/rotations. 68 | pub(crate) fn normalize_positions(crtcs: &mut Vec) { 69 | if crtcs.is_empty() { 70 | return; 71 | }; 72 | 73 | let left = crtcs.iter().map(|p| p.x).min().unwrap(); 74 | let top = crtcs.iter().map(|p| p.y).min().unwrap(); 75 | if (top, left) == (0, 0) { 76 | return; 77 | }; 78 | 79 | for c in crtcs.iter_mut() { 80 | c.offset((-left, -top)); 81 | } 82 | } 83 | 84 | // A wrapper that drops the pointer if it goes out of scope. 85 | // Avoid having to deal with the various early returns 86 | struct CrtcHandle { 87 | ptr: ptr::NonNull, 88 | } 89 | 90 | impl CrtcHandle { 91 | fn new(handle: &mut XHandle, xid: XId) -> Result { 92 | let res = ScreenResourcesHandle::new(handle)?; 93 | 94 | let raw_ptr = unsafe { xrandr::XRRGetCrtcInfo(handle.sys.as_ptr(), res.ptr(), xid) }; 95 | 96 | let ptr = ptr::NonNull::new(raw_ptr).ok_or(XrandrError::GetCrtcInfo(xid))?; 97 | 98 | Ok(Self { ptr }) 99 | } 100 | } 101 | 102 | impl Drop for CrtcHandle { 103 | fn drop(&mut self) { 104 | unsafe { xrandr::XRRFreeCrtcInfo(self.ptr.as_ptr()) }; 105 | } 106 | } 107 | 108 | impl Crtc { 109 | /// Open a handle to the lib-xrandr backend. This will be 110 | /// used for nearly all interactions with the xrandr lib 111 | /// 112 | /// # Arguments 113 | /// * `handle` - The xhandle to make the x calls with 114 | /// * `xid` - The internal XID of the requested crtc 115 | /// 116 | /// # Errors 117 | /// * `XrandrError::GetCrtc(xid)` - Could not find this xid. 118 | /// 119 | /// # Examples 120 | /// ``` 121 | /// let xhandle = XHandle.open()?; 122 | /// let mon1 = xhandle.monitors()?[0]; 123 | /// ``` 124 | /// 125 | pub fn from_xid(handle: &mut XHandle, xid: XId) -> Result { 126 | let crtc_info = CrtcHandle::new(handle, xid)?; 127 | 128 | let xrandr::XRRCrtcInfo { 129 | timestamp, 130 | x, 131 | y, 132 | width, 133 | height, 134 | mode, 135 | rotation, 136 | noutput, 137 | outputs, 138 | rotations, 139 | npossible, 140 | possible, 141 | } = unsafe { crtc_info.ptr.as_ref() }; 142 | 143 | let rotation = Rotation::try_from(*rotation)?; 144 | 145 | let outputs = unsafe { slice::from_raw_parts(*outputs, *noutput as usize) }; 146 | 147 | let possible = unsafe { slice::from_raw_parts(*possible, *npossible as usize) }; 148 | 149 | Ok(Self { 150 | xid, 151 | timestamp: *timestamp, 152 | x: *x, 153 | y: *y, 154 | width: *width, 155 | height: *height, 156 | mode: *mode, 157 | rotation, 158 | outputs: outputs.to_vec(), 159 | rotations: *rotations, 160 | possible: possible.to_vec(), 161 | }) 162 | } 163 | 164 | /// Apply the current fields of this crtc. `&mut self` needed to create a 165 | /// mut pointer to outputs, which lib-xrandr seems to require. 166 | /// # Examples 167 | /// ``` 168 | /// // Sets new mode on the crtc of some output 169 | /// let mut crtc = ScreenResources::new(self)?.crtc(self, output.crtc)?; 170 | /// crtc.mode = mode.xid; 171 | /// crtc.apply(xhandle) 172 | /// ``` 173 | /// 174 | pub(crate) fn apply(&mut self, handle: &mut XHandle) -> Result<(), XrandrError> { 175 | let outputs = match self.outputs.len() { 176 | 0 => std::ptr::null_mut(), 177 | _ => self.outputs.as_mut_ptr(), 178 | }; 179 | 180 | let res = ScreenResourcesHandle::new(handle)?; 181 | 182 | unsafe { 183 | xrandr::XRRSetCrtcConfig( 184 | handle.sys.as_ptr(), 185 | res.ptr(), 186 | self.xid, 187 | CURRENT_TIME, 188 | self.x, 189 | self.y, 190 | self.mode, 191 | self.rotation as u16, 192 | outputs, 193 | i32::try_from(self.outputs.len()).unwrap(), 194 | ); 195 | } 196 | 197 | Ok(()) 198 | } 199 | 200 | /// Alters some fields to reflect the disabled state 201 | /// Use apply() afterwards to actually disable the crtc 202 | pub(crate) fn set_disable(&mut self) { 203 | self.x = 0; 204 | self.y = 0; 205 | self.mode = 0; 206 | self.height = 0; 207 | self.width = 0; 208 | self.rotation = Rotation::Normal; 209 | self.outputs.clear(); 210 | } 211 | 212 | /// Width and height, accounting for a given rotation 213 | #[must_use] 214 | pub fn rotated_size(&self, rot: Rotation) -> (u32, u32) { 215 | let (w, h) = (self.width, self.height); 216 | 217 | let (old_w, old_h) = match self.rotation { 218 | Rotation::Normal | Rotation::Inverted => (w, h), 219 | Rotation::Left | Rotation::Right => (h, w), 220 | }; 221 | 222 | match rot { 223 | Rotation::Normal | Rotation::Inverted => (old_w, old_h), 224 | Rotation::Left | Rotation::Right => (old_h, old_w), 225 | } 226 | } 227 | 228 | /// The most down an dright coordinates that this crtc uses 229 | pub(crate) fn max_coordinates(&self) -> (i32, i32) { 230 | assert!( 231 | self.x >= 0 && self.y >= 0, 232 | "max_coordinates should be called on normalized crtc" 233 | ); 234 | 235 | // let (w, h) = self.rot_size(); 236 | // I think crtcs have this incorporated in their width/height fields 237 | (self.x + self.width as i32, self.y + self.height as i32) 238 | } 239 | 240 | /// Creates a new Crtc that is offset (.x and .y) fields, by offset param 241 | pub(crate) fn offset(&mut self, offset: (i32, i32)) { 242 | let x = i64::from(self.x) + i64::from(offset.0); 243 | let y = i64::from(self.y) + i64::from(offset.1); 244 | 245 | assert!( 246 | x < i64::from(i32::MAX) && y < i64::from(i32::MAX), 247 | "This offset would cause integer overflow" 248 | ); 249 | 250 | assert!(x >= 0 && y >= 0, "Invalid coordinates after offset"); 251 | 252 | self.x = i32::try_from(x).unwrap(); 253 | self.y = i32::try_from(y).unwrap(); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/components/LoadedScreen.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, MutableRefObject, SetStateAction, useRef, useState } from 'react'; 2 | import Select from 'react-select'; 3 | import { customSelectTheme, FrontendMonitor, MiniMonitor, Preset, Rotation } from '../globalValues'; 4 | import FreeHandPosition from './FreeHandPosition'; 5 | import './Loaded.css'; 6 | import FocusedMonitorSettings from './FocusedMonitorSettings'; 7 | import { invoke } from '@tauri-apps/api/core'; 8 | import ApplySettingsPopup from './Popups/ApplySettingsPopup'; 9 | import SimplePopUp from './Popups/SimplePopUp'; 10 | import Presets from './Presets'; 11 | import SingleErrorPopup, { SingleError } from './Popups/SingleErrorPopUp'; 12 | import MassApplyUndoPopup from './Popups/MassApplyUndoPopup'; 13 | import { cloneDeep } from 'lodash'; 14 | interface LoadedProps { 15 | singleErrorProps: SingleError; 16 | monitorRefreshRef: MutableRefObject; 17 | customMonitors: FrontendMonitor[]; 18 | initialMonitors: MutableRefObject; 19 | presets: Preset[]; 20 | setPresets: Dispatch>; 21 | setCustMonitors: Dispatch>; 22 | outputNames: MutableRefObject; 23 | } 24 | export interface focusedSettingsFunctions { 25 | enable: ((focusedMonitorIdx: number, enabled: boolean) => FrontendMonitor[]) | null; 26 | position: ((focusedMonitorIdx: number) => FrontendMonitor[]) | null; 27 | rotation: ((focusedMonitorIdx: number) => FrontendMonitor[]) | null; 28 | mode: ((focusedMonitorIdx: number) => FrontendMonitor[]) | null; 29 | setCrtc: ((focusedMonitorIdx: number, newCrtc: number) => FrontendMonitor[]) | null; 30 | } 31 | export const LoadedScreen: React.FC = ({ 32 | singleErrorProps, 33 | monitorRefreshRef, 34 | customMonitors, 35 | initialMonitors, 36 | presets, 37 | setPresets, 38 | setCustMonitors, 39 | outputNames, 40 | }) => { 41 | const [focusedMonitorIdx, setFocusedMonitorIdx] = useState(0); 42 | const resetFunctions = useRef({ 43 | enable: null, 44 | position: null, 45 | rotation: null, 46 | mode: null, 47 | setCrtc: null, 48 | }); 49 | const applyChangesRef = useRef< 50 | ((customMonitors: FrontendMonitor[], monitorsBeingApplied: number[]) => void) | null 51 | >(null); 52 | 53 | const normalizePositionsRef = useRef< 54 | ((customMonitors: FrontendMonitor[]) => FrontendMonitor[]) | null 55 | >(null); 56 | const rerenderMonitorsContainerRef = useRef<((customMonitors: FrontendMonitor[]) => void) | null>( 57 | null 58 | ); 59 | const [showSimplePopUp, setShowSimplePopUp] = useState(false); 60 | const [applyChangesPopupShowing, setApplyChangesPopupShowing] = useState(false); 61 | const [showMassApplyPopup, setShowMassUndoPopup] = useState(false); 62 | const [simplePopUpReason, setSimplePopUpReason] = useState('blah blah..'); 63 | const [monitorScale, setMonitorScale] = useState(10); 64 | //Collection handler 65 | async function applyAll() { 66 | console.log('apply all called'); 67 | await applyPrimaryMonitor(); 68 | if (applyChangesRef.current) { 69 | console.log('applying all exists'); 70 | setApplyChangesPopupShowing(true); 71 | await applyChangesRef.current( 72 | customMonitors, 73 | customMonitors.map((_mon, idx) => idx) 74 | ); 75 | setApplyChangesPopupShowing(false); 76 | } 77 | console.log('after initial:'); 78 | console.log(initialMonitors.current); 79 | } 80 | 81 | //PRIMARY MONITOR 82 | const monitorOptions = customMonitors.map(mon => { 83 | return { value: mon.name, label: mon.name }; 84 | }); 85 | function setPrimaryMonitor(newPrimName: String | undefined) { 86 | if (newPrimName) { 87 | setCustMonitors(mons => 88 | mons.map(mon => 89 | mon.name == newPrimName ? { ...mon, isPrimary: true } : { ...mon, isPrimary: false } 90 | ) 91 | ); 92 | } 93 | } 94 | function resetPrimryMonitor() { 95 | setCustMonitors(mons => 96 | mons.map((mon, idx) => ({ ...mon, isPrimary: initialMonitors.current[idx].isPrimary })) 97 | ); 98 | } 99 | async function applyPrimaryMonitor() { 100 | let newPrimaryIndex = customMonitors.findIndex(mon => mon.isPrimary); 101 | let OldPrimaryIndex = initialMonitors.current.findIndex(mon => mon.isPrimary); 102 | if (newPrimaryIndex != OldPrimaryIndex) { 103 | console.log('primary internal called'); 104 | await invoke('set_primary', { xid: customMonitors[newPrimaryIndex].outputs[0].xid }) 105 | .then(() => { 106 | initialMonitors.current[OldPrimaryIndex].isPrimary = false; 107 | initialMonitors.current[newPrimaryIndex].isPrimary = true; 108 | }) 109 | .catch(reason => { 110 | singleErrorProps.setShowSingleError(true); 111 | singleErrorProps.setSingleErrorText('Failed to set primary monitor due to ' + reason); 112 | }); 113 | } 114 | } 115 | 116 | function resetAll() { 117 | setCustMonitors(cloneDeep(initialMonitors.current)); 118 | if (rerenderMonitorsContainerRef.current) 119 | rerenderMonitorsContainerRef.current(initialMonitors.current); 120 | } 121 | function monitors2MiniMonitors(monitors: FrontendMonitor[]): MiniMonitor[] { 122 | return normalizePositionsRef.current!(monitors).map(mon => ({ 123 | output_xid: mon.outputs[0].xid, 124 | enabled: mon.outputs[0].enabled, 125 | rotation: mon.outputs[0].rotation, 126 | mode_xid: mon.outputs[0].currentMode.xid, 127 | mode_height: 128 | mon.outputs[0].rotation === Rotation.Normal || mon.outputs[0].rotation === Rotation.Inverted 129 | ? mon.outputs[0].currentMode.height 130 | : mon.outputs[0].currentMode.width, 131 | mode_width: 132 | mon.outputs[0].rotation === Rotation.Normal || mon.outputs[0].rotation === Rotation.Inverted 133 | ? mon.outputs[0].currentMode.width 134 | : mon.outputs[0].currentMode.height, 135 | x: mon.x.toFixed(0), 136 | y: mon.y.toFixed(0), 137 | })); 138 | } 139 | 140 | async function massApply() { 141 | setShowSimplePopUp(true); 142 | setSimplePopUpReason('Mass Applying'); 143 | await applyPrimaryMonitor(); 144 | let miniMonitors = monitors2MiniMonitors(customMonitors); 145 | //normalize all positions and pass 146 | await invoke<(number | undefined)[]>('quick_apply', { 147 | monitors: miniMonitors, 148 | }) 149 | .then(crtcs => { 150 | for (let i = 0; i < crtcs.length; i++) { 151 | if (crtcs[i]) { 152 | resetFunctions.current.setCrtc!(i, crtcs[i]!); 153 | } 154 | } 155 | setShowMassUndoPopup(true); 156 | }) 157 | .catch(err => { 158 | singleErrorProps.setShowSingleError(true); 159 | singleErrorProps.setSingleErrorText('Quick failed due to ' + err); 160 | }); 161 | } 162 | const customStyles = { 163 | control: (base: any) => ({ 164 | ...base, 165 | height: 52, 166 | minHeight: 52, 167 | }), 168 | }; 169 | function copyScript() { 170 | setShowSimplePopUp(true); 171 | setSimplePopUpReason('Creating Script'); 172 | let script: String = 'xrandr'; 173 | for (let i = 0; i < outputNames.current.length; i++) { 174 | let focusedMonitor = customMonitors.find( 175 | mon => mon.outputs[0].name === outputNames.current[i] 176 | ); 177 | console.log('focused monitor:'); 178 | console.log(outputNames.current[i]); 179 | console.log(focusedMonitor); 180 | if (focusedMonitor && focusedMonitor.outputs[0].enabled) { 181 | script += 182 | ' --output ' + 183 | outputNames.current[i] + 184 | ' --mode ' + 185 | focusedMonitor.outputs[0].currentMode.name + 186 | ' --rate ' + 187 | focusedMonitor.outputs[0].currentMode.rate + 188 | ' --pos ' + 189 | focusedMonitor.x + 190 | 'x' + 191 | focusedMonitor.y + 192 | ' --rotate ' + 193 | focusedMonitor.outputs[0].rotation.toLocaleLowerCase(); 194 | } else { 195 | script += ' --output ' + outputNames.current[i] + ' --off'; 196 | } 197 | navigator.clipboard.writeText(script.toString()); 198 | setShowSimplePopUp(false); 199 | } 200 | } 201 | 202 | return ( 203 |
212 |
213 | 220 | 229 | 237 | 244 | 251 |
252 |
253 |
254 |

263 | Primary Monitor: 264 |

265 | 278 | 281 | 284 |
285 |
286 |
287 | 297 | 307 |
308 |
309 |
310 |

Focused Monitor Settings

311 |
312 | {customMonitors.map((mon, idx) => { 313 | return ( 314 | 324 | ); 325 | })} 326 |
327 |
328 |
329 | 338 |
339 | 345 | 349 | 355 | 366 |
367 | ); 368 | }; 369 | export default LoadedScreen; 370 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{self, BufWriter}, 4 | path, 5 | }; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | use tokio::fs::{create_dir_all, read_dir, remove_file}; 9 | use xcap::{image::ImageError, XCapError}; 10 | use xrandr::{Crtc, Mode, Rotation, ScreenResources, XHandle, XId, XrandrError}; 11 | 12 | #[derive(Serialize, Deserialize, Debug)] 13 | struct Preset { 14 | name: String, 15 | monitors: Vec, 16 | } 17 | #[derive(Serialize, Deserialize, Debug)] 18 | struct FrontendMonitor { 19 | name: String, 20 | #[serde(rename = "imgSrc")] 21 | img_src: Option, 22 | #[serde(rename = "isPrimary")] 23 | is_primary: bool, 24 | x: i32, 25 | y: i32, 26 | #[serde(rename = "widthPx")] 27 | width_px: i32, 28 | #[serde(rename = "heightPx")] 29 | height_px: i32, 30 | /// An Output describes an actual physical monitor or display. A [`Monitor`] 31 | /// can have more than one output. 32 | outputs: Vec, 33 | } 34 | #[derive(Serialize, Deserialize, Debug)] 35 | struct FrontendOutput { 36 | xid: XId, 37 | #[serde(rename = "isPrimary")] 38 | is_primary: bool, 39 | enabled: bool, 40 | crtc: Option, 41 | //derived from crtc 42 | rotation: Rotation, 43 | name: String, 44 | connected: bool, 45 | modes: Vec, 46 | #[serde(rename = "preferredModes")] 47 | preferred_modes: Vec, 48 | #[serde(rename = "currentMode")] 49 | current_mode: Mode, 50 | } 51 | 52 | //dropping properties because its too extra for someone editing settings via gui to care 53 | fn mode_from_list(modes: Vec, mode_list: &Vec) -> Vec { 54 | return modes 55 | .into_iter() 56 | .filter(|mode| mode_list.contains(&mode.xid)) 57 | .collect(); 58 | } 59 | #[derive(Serialize, Debug)] 60 | enum GenericError { 61 | Serde(String), 62 | Xcap(String), 63 | Xrandr(XrandrError), 64 | Io(String), 65 | } 66 | 67 | impl From for GenericError { 68 | fn from(e: serde_json::Error) -> Self { 69 | GenericError::Serde(e.to_string()) 70 | } 71 | } 72 | impl From for GenericError { 73 | fn from(e: XrandrError) -> Self { 74 | GenericError::Xrandr(e) 75 | } 76 | } 77 | impl From for GenericError { 78 | fn from(e: XCapError) -> Self { 79 | GenericError::Xcap(e.to_string()) 80 | } 81 | } 82 | impl From for GenericError { 83 | fn from(e: ImageError) -> Self { 84 | GenericError::Xcap(e.to_string()) 85 | } 86 | } 87 | impl From for GenericError { 88 | fn from(e: io::Error) -> Self { 89 | GenericError::Io(e.to_string()) 90 | } 91 | } 92 | use lazy_static::lazy_static; 93 | lazy_static! { 94 | static ref app_directory_path: path::PathBuf = directories::BaseDirs::new() 95 | .unwrap() 96 | .config_dir() 97 | .join("display_settings_plus"); 98 | } 99 | #[tauri::command] 100 | async fn get_monitors() -> Result<(Vec, Vec), GenericError> { 101 | create_dir_all(app_directory_path.join("screenshots")).await?; 102 | create_dir_all(app_directory_path.join("presets")).await?; 103 | let mut xhandle = XHandle::open()?; 104 | let res = ScreenResources::new(&mut xhandle)?; 105 | let crtcs = res.crtcs(&mut xhandle)?; 106 | let modes = res.modes(); 107 | let outputs = res.outputs(&mut xhandle, Some(&crtcs), &res)?; 108 | let enabled_monitors: Vec = outputs 109 | .iter() 110 | .filter(|&out| out.connected && out.current_mode.is_some()) 111 | .map(|out| { 112 | let focused_crtc: Crtc = crtcs 113 | .iter() 114 | .find(|crtc| crtc.xid == out.crtc.unwrap()) 115 | .unwrap() 116 | .clone(); 117 | return FrontendMonitor { 118 | name: out.name.clone(), 119 | img_src: None, 120 | is_primary: out.is_primary, 121 | x: focused_crtc.x, 122 | y: focused_crtc.y, 123 | width_px: focused_crtc.width as i32, 124 | height_px: focused_crtc.height as i32, 125 | outputs: vec![FrontendOutput { 126 | xid: out.xid, 127 | is_primary: out.is_primary, 128 | crtc: out.crtc, 129 | enabled: out.current_mode.is_some(), 130 | rotation: focused_crtc.rotation.into(), 131 | name: out.name.clone(), 132 | connected: out.connected, 133 | modes: mode_from_list(modes.clone(), &out.modes), 134 | preferred_modes: mode_from_list(modes.clone(), &out.preferred_modes), 135 | current_mode: modes 136 | .iter() 137 | .find(|mode| mode.xid == out.current_mode.unwrap()) 138 | .unwrap() 139 | .clone(), 140 | }], 141 | }; 142 | }) 143 | .collect(); 144 | 145 | //handle inactive screens 146 | // 147 | let disabled_monitors: Vec = outputs 148 | .iter() 149 | .filter(|&out| out.connected && out.current_mode.is_none()) 150 | .map(|out| { 151 | let preferred_mode = modes 152 | .iter() 153 | .find(|mode| mode.xid == out.preferred_modes[0]) 154 | .unwrap(); 155 | FrontendMonitor { 156 | name: out.name.clone(), 157 | img_src: None, 158 | is_primary: false, 159 | x: 0, 160 | y: 0, 161 | width_px: preferred_mode.width as i32, 162 | height_px: preferred_mode.height as i32, 163 | outputs: vec![FrontendOutput { 164 | xid: out.xid, 165 | is_primary: out.is_primary, 166 | enabled: false, 167 | crtc: out.crtc, 168 | rotation: Rotation::Normal, 169 | name: out.name.clone(), 170 | connected: out.connected, 171 | modes: mode_from_list(modes.clone(), &out.modes), 172 | preferred_modes: mode_from_list(modes.clone(), &out.preferred_modes), 173 | current_mode: preferred_mode.clone(), 174 | }], 175 | } 176 | }) 177 | .collect::>(); 178 | 179 | let mut connected_monitors: Vec = Vec::new(); 180 | for monitor in enabled_monitors { 181 | connected_monitors.push(monitor); 182 | } 183 | for monitor in disabled_monitors { 184 | connected_monitors.push(monitor); 185 | } 186 | for (name, path) in take_screenshots()? { 187 | connected_monitors 188 | .iter_mut() 189 | .find(|mon| mon.name == name) 190 | .unwrap() 191 | .img_src = Some(path); 192 | } 193 | 194 | Ok(( 195 | connected_monitors, 196 | outputs.iter().map(|out| out.name.clone()).collect(), 197 | )) 198 | } 199 | 200 | fn take_screenshots() -> Result, ImageError> { 201 | let screenshot_monitors = xcap::Monitor::all().unwrap(); 202 | let mut paths: Vec<(String, String)> = Vec::new(); 203 | for monitor in screenshot_monitors { 204 | let cur_name = monitor.name(); 205 | let file_name = app_directory_path.join(format!("screenshots/{cur_name}.png")); 206 | paths.push((cur_name.to_owned(), file_name.to_str().unwrap().to_owned())); 207 | monitor.capture_image().unwrap().save(file_name)?; 208 | } 209 | Ok(paths) 210 | } 211 | #[tauri::command] 212 | async fn set_primary(xid: u64) -> Result<(), XrandrError> { 213 | let mut xhandle = XHandle::open()?; 214 | xhandle.set_primary(xid); 215 | return Ok(()); 216 | } 217 | //uses strings to parce because javascript round ,ciel and trunc make numbers like 1920.0 or 0.999999999999999999432 and im bored of it 218 | //Cannot realistically apply positions once at a time due to normalization pushing monitors to the left 219 | #[derive(Deserialize, Debug)] 220 | struct PositionProps { 221 | output_crtc: Option, 222 | x: String, 223 | y: String, 224 | } 225 | #[tauri::command] 226 | async fn set_positions(props: Vec) -> Result<(), XrandrError> { 227 | //sort props 228 | let mut crtcs: Vec = Vec::new(); 229 | let mut xhandle: XHandle = XHandle::open()?; 230 | let res = ScreenResources::new(&mut xhandle)?; 231 | println!("{:?}", props); 232 | for prop in props { 233 | if let Some(crtc_id) = prop.output_crtc { 234 | //setting up vars 235 | //shadow the dumb strings 236 | let x: i32 = prop.x.parse().unwrap(); 237 | let y: i32 = prop.y.parse().unwrap(); 238 | //making the change 239 | println!("position set x:{}, y:{}", x, y); 240 | let mut crtc = res.crtc(&mut xhandle, crtc_id)?; 241 | crtc.x = x; 242 | crtc.y = y; 243 | crtcs.push(crtc); 244 | } else { 245 | println!("did not update cus no id"); 246 | } 247 | } 248 | xhandle.apply_new_crtcs(&mut crtcs, &res)?; 249 | return Ok(()); 250 | } 251 | #[tauri::command] 252 | async fn set_rotation( 253 | output_crtc: Option, 254 | rotation: Rotation, 255 | new_width: u32, 256 | new_height: u32, 257 | ) -> Result<(), XrandrError> { 258 | //setting up vars 259 | println!( 260 | "Called Set Rotation with crtc id {:#?} and rotation {:#?}", 261 | output_crtc, rotation 262 | ); 263 | if let Some(crtc_id) = output_crtc { 264 | let mut xhandle = XHandle::open()?; 265 | let res = ScreenResources::new(&mut xhandle)?; 266 | let mut crtc = res.crtc(&mut xhandle, crtc_id)?; 267 | crtc.rotation = rotation; 268 | println!("old width:{},height:{}", crtc.width, crtc.height); 269 | crtc.width = new_width; 270 | crtc.height = new_height; 271 | println!("new width:{},height:{}", crtc.width, crtc.height); 272 | xhandle.apply_new_crtcs(&mut [crtc], &res)?; 273 | } else { 274 | return Err(XrandrError::OutputDisabled("".to_owned())); 275 | } 276 | return Ok(()); 277 | } 278 | ///returns crtc after enabling monitor 279 | #[tauri::command] 280 | async fn set_enabled(xid: u64, enabled: bool) -> Result { 281 | //setting up vars 282 | println!("enabled set as {} for {}", enabled, xid); 283 | let mut xhandle = XHandle::open()?; 284 | let res = ScreenResources::new(&mut xhandle)?; 285 | let focused_output = res.output(&mut xhandle, xid, None, &res)?; 286 | //making the change 287 | let mut new_crtc = 0; 288 | if enabled { 289 | new_crtc = xhandle.enable(&focused_output, &res)?; 290 | } else { 291 | xhandle.disable(&focused_output, &res)?; 292 | } 293 | return Ok(new_crtc); 294 | } 295 | #[tauri::command] 296 | async fn set_mode( 297 | output_crtc: Option, 298 | mode_xid: u64, 299 | mode_height: u32, 300 | mode_width: u32, 301 | ) -> Result<(), XrandrError> { 302 | if let Some(crtc_id) = output_crtc { 303 | let mut xhandle = XHandle::open()?; 304 | let res = ScreenResources::new(&mut xhandle)?; 305 | xhandle.set_mode(crtc_id, mode_xid, mode_height, mode_width, &res)?; 306 | } else { 307 | return Err(XrandrError::OutputDisabled("".to_owned())); 308 | } 309 | return Ok(()); 310 | } 311 | 312 | #[tauri::command] 313 | async fn get_presets() -> Result, GenericError> { 314 | let mut presets: Vec = Vec::new(); 315 | let mut files_in_presets = read_dir(app_directory_path.join(format!("presets/"))).await?; 316 | while let Some(file) = files_in_presets.next_entry().await? { 317 | let file_path = file.path(); 318 | if file_path.extension().and_then(|ext| ext.to_str()) == Some("json") { 319 | let file_name = file.file_name().to_string_lossy().to_string(); 320 | let file_content = tokio::fs::read_to_string(&file_path).await?; 321 | let monitors = serde_json::from_str::>(&file_content)?; 322 | let name = file_name[..file_name.len() - 5].to_owned(); 323 | presets.push(Preset { name, monitors }) 324 | } 325 | } 326 | return Ok(presets); 327 | } 328 | ///Used by both overwrite preset and create preset 329 | #[tauri::command] 330 | async fn create_preset(preset: Preset) -> Result<(), GenericError> { 331 | let file_name = app_directory_path.join(format!("presets/{}.json", preset.name)); 332 | let new_file = File::create(file_name)?; 333 | let mut writer = BufWriter::new(new_file); 334 | serde_json::to_writer(&mut writer, &preset.monitors)?; 335 | io::Write::flush(&mut writer)?; 336 | return Ok(()); 337 | } 338 | #[tauri::command] 339 | async fn delete_preset(preset_name: String) -> Result<(), GenericError> { 340 | let file_name = app_directory_path.join(format!("presets/{preset_name}.json")); 341 | remove_file(file_name).await?; 342 | return Ok(()); 343 | } 344 | #[derive(Deserialize, Debug)] 345 | struct MiniMonitor { 346 | output_xid: u64, 347 | enabled: bool, 348 | rotation: Rotation, 349 | mode_xid: u64, 350 | mode_height: u32, 351 | mode_width: u32, 352 | x: String, 353 | y: String, 354 | } 355 | #[tauri::command] 356 | async fn quick_apply(monitors: Vec) -> Result>, XrandrError> { 357 | let mut xhandle = XHandle::open()?; 358 | let mut crtcs_changed: Vec = Vec::new(); 359 | let mut crtc_ids: Vec> = Vec::new(); 360 | let res = ScreenResources::new(&mut xhandle).unwrap(); 361 | let crtcs = res.crtcs(&mut xhandle)?; 362 | let outputs = res.outputs(&mut xhandle, Some(&crtcs), &res)?; 363 | println!("Mass/Quick Applying:"); 364 | println!("{:#?}", monitors); 365 | for current_monitor in monitors { 366 | let current_output = outputs 367 | .iter() 368 | .find(|out| out.xid == current_monitor.output_xid) 369 | .unwrap(); 370 | let mut crtc: Crtc; 371 | if current_monitor.enabled { 372 | //enable 373 | if current_output.current_mode.is_some() && current_output.crtc.is_some() { 374 | //crtc was already enabled 375 | crtc = crtcs 376 | .iter() 377 | .find(|crt| crt.xid == current_output.crtc.unwrap()) 378 | .unwrap() 379 | .clone(); 380 | } else { 381 | //crtc needs to be enabled 382 | crtc = xhandle.find_available_crtc(¤t_output, &res)?; 383 | crtc.outputs = vec![current_output.xid]; 384 | } 385 | //position 386 | crtc.x = current_monitor.x.parse().unwrap(); 387 | crtc.y = current_monitor.y.parse().unwrap(); 388 | //mode 389 | crtc.mode = current_monitor.mode_xid; 390 | crtc.height = current_monitor.mode_height; 391 | crtc.width = current_monitor.mode_width; 392 | //rotation 393 | crtc.rotation = current_monitor.rotation; 394 | //finalize 395 | crtc_ids.push(Some(crtc.xid)); 396 | crtcs_changed.push(crtc); 397 | } else { 398 | //Disable 399 | if let Some(crtc_id) = current_output.crtc { 400 | let mut crtc = crtcs.iter().find(|crt| crt.xid == crtc_id).unwrap().clone(); 401 | crtc.x = 0; 402 | crtc.y = 0; 403 | crtc.mode = 0; 404 | crtc.rotation = Rotation::Normal; 405 | crtc.outputs.clear(); 406 | crtcs_changed.push(crtc); 407 | } 408 | crtc_ids.push(None); 409 | } 410 | } 411 | xhandle.apply_new_crtcs(&mut crtcs_changed, &res)?; 412 | Ok(crtc_ids) 413 | } 414 | //TODO: add a script maker 415 | //https://askubuntu.com/questions/63681/how-can-i-make-xrandr-customization-permanent 416 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 417 | pub fn run() { 418 | tauri::Builder::default() 419 | .plugin(tauri_plugin_opener::init()) 420 | .invoke_handler(tauri::generate_handler![ 421 | get_monitors, 422 | set_primary, 423 | set_enabled, 424 | set_positions, 425 | set_rotation, 426 | set_mode, 427 | get_presets, 428 | create_preset, 429 | delete_preset, 430 | quick_apply 431 | ]) 432 | .run(tauri::generate_context!()) 433 | .expect("error while running tauri application"); 434 | } 435 | #[cfg(test)] 436 | mod tests { 437 | use std::time::Instant; 438 | 439 | use super::*; 440 | 441 | #[tokio::test] 442 | async fn speed_test_for_outputs() { 443 | let start = Instant::now(); 444 | let _monitors = get_monitors().await.unwrap(); 445 | println!("Time taken: {:#?}", start.elapsed()) 446 | } 447 | 448 | // #[test] 449 | // fn can_debug_format_monitors() { 450 | // format!("{:#?}", handle().monitors().unwrap()); 451 | // } 452 | } 453 | -------------------------------------------------------------------------------- /src-tauri/xrandr/src/lib.rs: -------------------------------------------------------------------------------- 1 | use itertools::EitherOrBoth as ZipEntry; 2 | use itertools::Itertools; 3 | use serde::Serialize; 4 | use std::collections::HashMap; 5 | use std::ffi::CStr; 6 | use std::fmt::Debug; 7 | use std::os::raw::c_ulong; 8 | use std::ptr; 9 | 10 | use crtc::normalize_positions; 11 | pub use indexmap; 12 | pub use screen_resources::ScreenResources; 13 | use thiserror::Error; 14 | use x11::{xlib, xrandr}; 15 | 16 | pub use crate::crtc::Crtc; 17 | pub use crate::crtc::{Relation, Rotation}; 18 | pub use crate::mode::Mode; 19 | pub use crate::monitor::Monitor; 20 | use crate::monitor::MonitorHandle; 21 | pub use crate::screensize::ScreenSize; 22 | pub use output::Output; 23 | 24 | mod crtc; 25 | mod mode; 26 | mod monitor; 27 | mod output; 28 | mod screen_resources; 29 | mod screensize; 30 | 31 | // All retrieved information is timestamped by when that information was 32 | // last changed in the backend. If we alter an object (e.g. crtc, output) we 33 | // have to pass the timestamp we got with it. If the x backend detects that 34 | // changes have occured since we retrieved the information, our new change 35 | // will not go through. 36 | pub type XTime = c_ulong; 37 | // Xrandr seems to want the time `0` when calling setter functions 38 | const CURRENT_TIME: c_ulong = 0; 39 | // Unique identifiers for the various objects in the x backend 40 | // (crtcs,outputs,modes, etc.) 41 | pub type XId = c_ulong; 42 | 43 | // The main handle consists simply of a pointer to the display 44 | type HandleSys = ptr::NonNull; 45 | #[derive(Debug)] 46 | pub struct XHandle { 47 | sys: HandleSys, 48 | } 49 | 50 | impl XHandle { 51 | /// Open a handle to the lib-xrandr backend. This will be 52 | /// used for nearly all interactions with the xrandr lib 53 | /// 54 | /// # Errors 55 | /// * `XrandrError::Open` - Getting the handle failed. 56 | /// 57 | /// # Examples 58 | /// ``` 59 | /// let xhandle = XHandle.open()?; 60 | /// let mon1 = xhandle.monitors()?[0]; 61 | /// ``` 62 | /// 63 | pub fn open() -> Result { 64 | // XOpenDisplay argument is screen name 65 | // Null pointer gets first display? 66 | let sys = ptr::NonNull::new(unsafe { xlib::XOpenDisplay(ptr::null()) }) 67 | .ok_or(XrandrError::Open)?; 68 | 69 | Ok(Self { sys }) 70 | } 71 | 72 | /// List every monitor 73 | /// 74 | /// # Errors 75 | /// * `XrandrError::_` - various calls to the xrandr backend may fail 76 | /// 77 | /// # Examples 78 | /// ``` 79 | /// let mon1 = xhandle.monitors()?[0]; 80 | /// ``` 81 | /// 82 | pub fn monitors(&mut self, res: &ScreenResources) -> Result, XrandrError> { 83 | let infos = MonitorHandle::new(self)?; 84 | 85 | infos 86 | .as_slice() 87 | .iter() 88 | .map(|sys| { 89 | let outputs = unsafe { Output::from_list(self, sys.outputs, sys.noutput, &res) }?; 90 | 91 | Ok(Monitor { 92 | name: atom_name(&mut self.sys, sys.name)?, 93 | is_primary: real_bool(sys.primary), 94 | is_automatic: real_bool(sys.automatic), 95 | x: sys.x, 96 | y: sys.y, 97 | width_px: sys.width, 98 | height_px: sys.height, 99 | width_mm: sys.mwidth, 100 | height_mm: sys.mheight, 101 | outputs, 102 | }) 103 | }) 104 | .collect::>() 105 | } 106 | 107 | // TODO: this seems to be more complicated in xrandr.c 108 | // Finds an available Crtc for a given (disabled) output 109 | pub fn find_available_crtc( 110 | &mut self, 111 | o: &Output, 112 | res: &ScreenResources, 113 | ) -> Result { 114 | let crtcs = res.crtcs(self)?; 115 | 116 | for crtc in crtcs { 117 | if crtc.possible.contains(&o.xid) && crtc.outputs.is_empty() { 118 | return Ok(crtc); 119 | } 120 | } 121 | 122 | Err(XrandrError::NoCrtcAvailable) 123 | } 124 | 125 | /// Enable the given output by setting it to its preferred mode 126 | /// 127 | /// # Errors 128 | /// * `XrandrError::_` - various calls to the xrandr backend may fail 129 | /// 130 | /// # Examples 131 | /// ``` 132 | /// let dp_1 = xhandle.all_outputs()?[0]; 133 | /// xhandle.enable(dp_1)?; 134 | /// ``` 135 | /// 136 | pub fn enable(&mut self, output: &Output, res: &ScreenResources) -> Result { 137 | if output.current_mode.is_some() { 138 | return Ok(output.crtc.unwrap()); 139 | } 140 | 141 | let target_mode = output 142 | .preferred_modes 143 | .first() 144 | .ok_or(XrandrError::NoPreferredModes(output.xid))?; 145 | 146 | let mut crtc = self.find_available_crtc(output, res)?; 147 | let mode = res.mode(*target_mode)?; 148 | crtc.mode = mode.xid; 149 | crtc.width = mode.width; 150 | crtc.height = mode.height; 151 | crtc.outputs = vec![output.xid]; 152 | self.apply_new_crtcs(&mut [crtc.clone()], res)?; 153 | Ok(crtc.xid) 154 | } 155 | 156 | /// Disable the given output 157 | /// 158 | /// # Errors 159 | /// * `XrandrError::_` - various calls to the xrandr backend may fail 160 | /// 161 | /// # Examples 162 | /// ``` 163 | /// let dp_1 = xhandle.all_outputs()?[0]; 164 | /// xhandle.disable(dp_1)?; 165 | /// ``` 166 | /// 167 | pub fn disable(&mut self, output: &Output, res: &ScreenResources) -> Result<(), XrandrError> { 168 | let crtc_id = match output.crtc { 169 | None => { 170 | println!("monitor was already disabled"); 171 | return Ok(()); 172 | } 173 | Some(xid) => xid, 174 | }; 175 | let mut crtc = res.crtc(self, crtc_id)?; 176 | crtc.set_disable(); 177 | 178 | self.apply_new_crtcs(&mut [crtc], &res) 179 | } 180 | 181 | /// Sets the given output as the primary output 182 | /// 183 | /// # Errors 184 | /// * `XrandrError::_` - various calls to the xrandr backend may fail 185 | /// 186 | /// # Examples 187 | /// ``` 188 | /// let dp_1 = xhandle.all_outputs()?[0]; 189 | /// xhandle.set_primary(dp_1)?; 190 | /// ``` 191 | /// localy edited to use xid upfront to save the time of passing objects back and forth 192 | pub fn set_primary(&mut self, xid: u64) { 193 | unsafe { 194 | xrandr::XRRSetOutputPrimary(self.sys.as_ptr(), self.root(), xid); 195 | } 196 | } 197 | 198 | // - xrandr does not seem to resize after a rotation, and this feels 199 | // similar to me. I would say let the user reposition the displays 200 | /// Sets the mode of a given output, relative to another 201 | /// 202 | /// # Arguments 203 | /// * `output` - The output to change mode for 204 | /// * `mode` - The mode to change to 205 | /// 206 | /// # Errors 207 | /// * `XrandrError::_` - various calls to the xrandr backend may fail 208 | /// 209 | /// # Examples 210 | /// ``` 211 | /// let dp_1 = xhandle.all_outputs()?[0]; 212 | /// let mode = dp_1.preferred_modes[0]; 213 | /// xhandle.set_mode(dp_1, mode)?; 214 | /// ``` 215 | /// 216 | pub fn set_mode( 217 | &mut self, 218 | output_crtc: XId, 219 | mode_xid: XId, 220 | height: u32, 221 | width: u32, 222 | res: &ScreenResources, 223 | ) -> Result<(), XrandrError> { 224 | //Created a pull that fixed this method at https://github.com/dzfranklin/xrandr-rs/pull/19 225 | let mut crtc = res.crtc(self, output_crtc)?; 226 | crtc.mode = mode_xid; 227 | crtc.height = height; 228 | crtc.width = width; 229 | // 230 | self.apply_new_crtcs(&mut [crtc], res) 231 | } 232 | 233 | /// Sets the position of a given output, relative to another 234 | /// 235 | /// # Arguments 236 | /// * `output` - The output to reposition 237 | /// * `relation` - The relation `output` will have to `rel_output` 238 | /// * `rel_output` - The output to position relative to 239 | /// 240 | /// # Errors 241 | /// * `XrandrError::_` - various calls to the xrandr backend may fail 242 | /// 243 | /// # Examples 244 | /// ``` 245 | /// let dp_1 = outputs[0]; 246 | /// let hdmi_1 = outputs[3]; 247 | /// xhandle.set_position(dp_1, Relation::LeftOf, hdmi_1)?; 248 | /// ``` 249 | /// 250 | pub fn set_position_relative( 251 | &mut self, 252 | output: &Output, 253 | relation: &Relation, 254 | relative_output: &Output, 255 | ) -> Result<(), XrandrError> { 256 | let crtc_id = output 257 | .crtc 258 | .ok_or(XrandrError::OutputDisabled(output.name.clone()))?; 259 | let rel_crtc_id = relative_output 260 | .crtc 261 | .ok_or(XrandrError::OutputDisabled(relative_output.name.clone()))?; 262 | 263 | let res = ScreenResources::new(self)?; 264 | let mut crtc = res.crtc(self, crtc_id)?; 265 | let rel_crtc = res.crtc(self, rel_crtc_id)?; 266 | 267 | // Calculate new (x,y) based on: 268 | // - own width/height & relative output's width/height/x/y 269 | let (w, h) = (crtc.width as i32, crtc.height as i32); 270 | let (rel_w, rel_h) = (rel_crtc.width as i32, rel_crtc.height as i32); 271 | let (rel_x, rel_y) = (rel_crtc.x, rel_crtc.y); 272 | 273 | (crtc.x, crtc.y) = match relation { 274 | Relation::LeftOf => (rel_x - w, rel_y), 275 | Relation::RightOf => (rel_x + rel_w, rel_y), 276 | Relation::Above => (rel_x, rel_y - h), 277 | Relation::Below => (rel_x, rel_y + rel_h), 278 | Relation::SameAs => (rel_x, rel_y), 279 | }; 280 | 281 | self.apply_new_crtcs(&mut [crtc], &res) 282 | } 283 | 284 | /// Sets the position of a given output, relative to another 285 | /// 286 | /// # Arguments 287 | /// * `output` - The output to rotate 288 | /// * `rotation` 289 | /// 290 | /// # Errors 291 | /// * `XrandrError::_` - various calls to the xrandr backend may fail 292 | /// 293 | /// # Examples 294 | /// ``` 295 | /// let dp_1 = outputs[0]; 296 | /// xhandle.set_rotation(dp_1, Rotation::Inverted)?; 297 | /// ``` 298 | /// 299 | pub fn set_rotation(&mut self, crtc_id: u64, rotation: &Rotation) -> Result<(), XrandrError> { 300 | println!("Rotation Called"); 301 | let res = ScreenResources::new(self)?; 302 | let mut crtc = res.crtc(self, crtc_id)?; 303 | crtc.rotation = *rotation; 304 | (crtc.width, crtc.height) = crtc.rotated_size(*rotation); 305 | 306 | self.apply_new_crtcs(&mut [crtc], &res) 307 | } 308 | 309 | /// Applies some set of altered crtcs 310 | /// Due to xrandr's structure, changing one or more crtcs properly can be 311 | /// quite complicated. One should therefore call this function on any crtcs 312 | /// that you want to change. 313 | /// # Arguments 314 | /// * `changes` 315 | /// Altered crtcs. Must be mutable because of crct.apply() calls. 316 | /// 317 | pub fn apply_new_crtcs( 318 | &mut self, 319 | changed: &mut [Crtc], 320 | res: &ScreenResources, 321 | ) -> Result<(), XrandrError> { 322 | let old_crtcs = res.enabled_crtcs(self)?; 323 | 324 | // Construct new crtcs out of the old ones and the new where provided 325 | // turning changed CRTC into a hashmap 326 | let mut changed_map: HashMap = HashMap::new(); 327 | changed.iter().cloned().for_each(|c| { 328 | changed_map.insert(c.xid, c); 329 | }); 330 | 331 | let mut new_crtcs: Vec = Vec::new(); 332 | for crtc in &old_crtcs { 333 | match changed_map.remove(&crtc.xid) { 334 | None => new_crtcs.push(crtc.clone()), 335 | Some(c) => new_crtcs.push(c.clone()), 336 | } 337 | } 338 | new_crtcs.extend(changed_map.drain().map(|(_, v)| v)); 339 | 340 | // In case the top-left corner is no longer at (0,0), renormalize 341 | normalize_positions(&mut new_crtcs); 342 | let old_size = ScreenSize::fitting_crtcs(self, &old_crtcs); 343 | println!("old screen size: {:?}", old_size); 344 | let new_size = ScreenSize::fitting_crtcs(self, &new_crtcs); 345 | println!("new screen size: {:?}", new_size); 346 | // Disable crtcs that do not fit before setting the new size 347 | // Note that this should only be crtcs that were changed, but `changed` 348 | // contains the already altered crtc, so we have to use `old_crtcs` 349 | let mut old_crtcs = old_crtcs; 350 | 351 | for crtc in &mut old_crtcs { 352 | if !new_size.fits_crtc(crtc) { 353 | crtc.set_disable(); 354 | crtc.apply(self)?; 355 | } 356 | } 357 | self.set_screensize(&new_size); 358 | 359 | // Find the crtcs that were changed. Done this late to also account 360 | // for crtcs that were altered by normalize_positions() 361 | let mut to_apply: Vec<&mut Crtc> = Vec::new(); 362 | for pair in old_crtcs.iter().zip_longest(new_crtcs.iter_mut()) { 363 | match pair { 364 | ZipEntry::Both(old, new) => { 365 | assert!(old.xid == new.xid, "invalid new_crtcs"); 366 | if new.timestamp < old.timestamp { 367 | return Err(XrandrError::CrtcChanged(new.xid)); 368 | } 369 | if new != old { 370 | to_apply.push(new); 371 | } 372 | } 373 | ZipEntry::Right(new) => to_apply.push(new), 374 | ZipEntry::Left(_) => unreachable!("invalid new_crtcs"), 375 | } 376 | } 377 | 378 | // Move and re-enable the crtcs 379 | to_apply.iter_mut().try_for_each(|c| c.apply(self)) 380 | } 381 | 382 | /// Sets the screen size in the x backend 383 | fn set_screensize(&mut self, size: &ScreenSize) { 384 | unsafe { 385 | xrandr::XRRSetScreenSize( 386 | self.sys.as_ptr(), 387 | self.root(), 388 | size.width, 389 | size.height, 390 | size.width_mm, 391 | size.height_mm, 392 | ); 393 | } 394 | } 395 | 396 | fn root(&mut self) -> c_ulong { 397 | unsafe { xlib::XDefaultRootWindow(self.sys.as_ptr()) } 398 | } 399 | } 400 | 401 | impl Drop for XHandle { 402 | fn drop(&mut self) { 403 | unsafe { xlib::XCloseDisplay(self.sys.as_ptr()) }; 404 | } 405 | } 406 | 407 | fn real_bool(sys: xlib::Bool) -> bool { 408 | assert!( 409 | sys == 0 || sys == 1, 410 | "Integer larger than 1 does not represent a bool" 411 | ); 412 | sys == 1 413 | } 414 | 415 | fn atom_name(handle: &mut HandleSys, atom: xlib::Atom) -> Result { 416 | let chars = ptr::NonNull::new(unsafe { xlib::XGetAtomName(handle.as_ptr(), atom) }) 417 | .ok_or(XrandrError::GetAtomName(atom))?; 418 | 419 | let name = unsafe { CStr::from_ptr(chars.as_ptr()) } 420 | .to_string_lossy() 421 | .to_string(); 422 | 423 | unsafe { 424 | xlib::XFree(chars.as_ptr().cast()); 425 | } 426 | 427 | Ok(name) 428 | } 429 | 430 | #[derive(Error, Debug, Serialize)] 431 | pub enum XrandrError { 432 | #[error("Failed to open connection to x11.")] 433 | Open, 434 | 435 | #[error("Call to XRRGetMonitors failed.")] 436 | GetMonitors, 437 | 438 | #[error("No CRTC available to put onto new output")] 439 | NoCrtcAvailable, 440 | 441 | #[error("Call to XRRGetScreenResources for XRRDefaultRootWindow failed")] 442 | GetResources, 443 | 444 | #[error("The output '{0}' is disabled")] 445 | OutputDisabled(String), 446 | 447 | #[error("Invalid rotation: {0}")] 448 | InvalidRotation(u16), 449 | 450 | #[error("Could not get info on mode with xid {0}")] 451 | GetMode(xlib::XID), 452 | 453 | #[error("Crtc changed since last requesting its state")] 454 | CrtcChanged(xlib::XID), 455 | 456 | #[error("Call to XRRGetCrtcInfo for CRTC with xid {0} failed")] 457 | GetCrtcInfo(xlib::XID), 458 | 459 | #[error("Failed to get Crtc: No Crtc with ID {0}")] 460 | GetCrtc(xlib::XID), 461 | 462 | #[error("Call to XRRGetOutputInfo for output with xid {0} failed")] 463 | GetOutputInfo(xlib::XID), 464 | 465 | #[error("No preferred modes found for output with xid {0}")] 466 | NoPreferredModes(xlib::XID), 467 | 468 | #[error("Unable to fit the settings into the screen space")] 469 | FitCrit, 470 | 471 | #[error("No mode found with xid {0}")] 472 | GetModeInfo(xlib::XID), 473 | 474 | #[error("Failed to get the properties of output with xid {0}")] 475 | GetOutputProp(xlib::XID), 476 | 477 | #[error("Failed to name of atom {0}")] 478 | GetAtomName(xlib::Atom), 479 | } 480 | 481 | #[cfg(test)] 482 | mod tests { 483 | use super::*; 484 | 485 | fn handle() -> XHandle { 486 | XHandle::open().unwrap() 487 | } 488 | 489 | #[test] 490 | fn can_open() { 491 | handle(); 492 | } 493 | 494 | // #[test] 495 | // fn can_debug_format_monitors() { 496 | // format!("{:#?}", handle().monitors().unwrap()); 497 | // } 498 | } 499 | -------------------------------------------------------------------------------- /src/components/FocusedMonitorSettings.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, MutableRefObject, SetStateAction, useEffect, useRef } from 'react'; 2 | import { customSelectTheme, FrontendMonitor, Mode, Rotation } from '../globalValues'; 3 | import './FocusedMonitorSettings.css'; 4 | import Select from 'react-select'; 5 | import { focusedSettingsFunctions } from './LoadedScreen'; 6 | interface FocusedMonitorSettingsProps { 7 | focusedMonitorIdx: number; 8 | customMonitors: FrontendMonitor[]; 9 | initialMonitors: MutableRefObject; 10 | monitorScale: number; 11 | setMonitors: Dispatch>; 12 | rerenderMonitorsContainerRef: MutableRefObject<((newMonitors: FrontendMonitor[]) => void) | null>; 13 | resetFunctions: MutableRefObject; 14 | } 15 | export const FocusedMonitorSettings: React.FC = ({ 16 | focusedMonitorIdx, 17 | customMonitors, 18 | initialMonitors, 19 | monitorScale, 20 | setMonitors, 21 | rerenderMonitorsContainerRef, 22 | resetFunctions, 23 | }) => { 24 | let lastStateWhenRerenderCalled = useRef([]); 25 | let lastMonitorScaleWhenRerenderCalled = useRef(0); 26 | //if there are any size changes, then the monitors need to rerendered without affecting the order integrity or stretching 27 | //MANUAL ATTEMPT 28 | useEffect(() => { 29 | if ( 30 | rerenderMonitorsContainerRef.current && 31 | (customMonitors !== lastStateWhenRerenderCalled.current || 32 | lastMonitorScaleWhenRerenderCalled.current !== monitorScale) 33 | ) { 34 | lastStateWhenRerenderCalled.current = [...customMonitors]; 35 | lastMonitorScaleWhenRerenderCalled.current = monitorScale; 36 | rerenderMonitorsContainerRef.current(customMonitors); 37 | } 38 | }, [customMonitors, monitorScale]); 39 | useEffect(() => { 40 | resetFunctions.current.enable = setEnabled; 41 | resetFunctions.current.position = resetPosition; 42 | resetFunctions.current.rotation = resetRotation; 43 | resetFunctions.current.mode = resetModePreset; 44 | resetFunctions.current.setCrtc = setCrtc; 45 | }, [setEnabled, resetPosition, resetRotation, resetModePreset]); 46 | //Enable 47 | ///used to disable the rest of the options: 48 | function setEnabled(focusedMonitorIdx: number, enabled: boolean): FrontendMonitor[] { 49 | console.log(initialMonitors.current); 50 | console.log(customMonitors); 51 | let newCustMonitors = customMonitors; 52 | if (enabled) { 53 | //updating page state 54 | let modeXid = initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.xid; 55 | let modeWidth = initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.width; 56 | let modeHeight = initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.height; 57 | //if was previously disabled and saved(ot ensure the diabled mode does not get pushed again) 58 | if (modeXid === 0) { 59 | console.log('tried to push a diabled, but was corrected'); 60 | let preferredMode = initialMonitors.current[focusedMonitorIdx].outputs[0].preferredModes[0]; 61 | modeXid = preferredMode.xid; 62 | modeWidth = preferredMode.width; 63 | modeHeight = preferredMode.height; 64 | } 65 | newCustMonitors = customMonitors.map((mon, idx) => 66 | idx === focusedMonitorIdx 67 | ? { 68 | ...mon, 69 | x: initialMonitors.current[focusedMonitorIdx].x, 70 | y: initialMonitors.current[focusedMonitorIdx].y, 71 | outputs: mon.outputs.map((out, outIdx) => 72 | outIdx === 0 73 | ? { 74 | ...out, 75 | enabled: true, 76 | rotation: initialMonitors.current[focusedMonitorIdx].outputs[0].rotation, 77 | currentMode: { 78 | ...out.currentMode, 79 | xid: modeXid, 80 | width: modeWidth, 81 | height: modeHeight, 82 | }, 83 | } 84 | : out 85 | ), 86 | } 87 | : mon 88 | ); 89 | } else { 90 | //what happens inside the xrandr library: 91 | /* 92 | self.x = 0; 93 | self.y = 0; 94 | self.mode = 0; 95 | self.rotation = Rotation::Normal; 96 | self.outputs.clear(); 97 | */ 98 | newCustMonitors = customMonitors.map((mon, idx) => 99 | idx === focusedMonitorIdx 100 | ? { 101 | ...mon, 102 | x: 0, 103 | y: 0, 104 | outputs: mon.outputs.map((out, outIdx) => 105 | outIdx === 0 106 | ? { 107 | ...out, 108 | rotation: Rotation.Normal, 109 | enabled: false, 110 | currentMode: { 111 | ...out.currentMode, 112 | width: 0, 113 | height: 0, 114 | xid: 0, 115 | }, 116 | } 117 | : out 118 | ), 119 | } 120 | : mon 121 | ); 122 | } 123 | setMonitors(newCustMonitors); 124 | return newCustMonitors; 125 | } 126 | function setCrtc(focusedMonitorIdx: number, newCrtc: number): FrontendMonitor[] { 127 | let newCustomMonitors = customMonitors.map((curMon, idx) => 128 | idx === focusedMonitorIdx 129 | ? { 130 | ...curMon, 131 | outputs: curMon.outputs.map((out, outIdx) => 132 | outIdx == 0 ? { ...out, crtc: newCrtc } : out 133 | ), 134 | } 135 | : curMon 136 | ); 137 | setMonitors(newCustomMonitors); 138 | initialMonitors.current[focusedMonitorIdx].outputs[0].crtc = newCrtc; 139 | return newCustomMonitors; 140 | } 141 | //POSITIONS 142 | function setPositionX(x: number) { 143 | x = Math.trunc(x); 144 | setMonitors(mons => 145 | mons.map((curMon, idx) => (idx === focusedMonitorIdx ? { ...curMon, x: x } : curMon)) 146 | ); 147 | } 148 | function setPositionY(y: number) { 149 | y = Math.trunc(y); 150 | setMonitors(mons => 151 | mons.map((curMon, idx) => (idx === focusedMonitorIdx ? { ...curMon, y } : curMon)) 152 | ); 153 | } 154 | function resetPosition(focusedMonitorIdx: number): FrontendMonitor[] { 155 | console.log(initialMonitors.current); 156 | console.log(customMonitors); 157 | let newCustMonitors = customMonitors.map((curMon, idx) => 158 | idx === focusedMonitorIdx 159 | ? { ...curMon, x: initialMonitors.current[idx].x, y: initialMonitors.current[idx].y } 160 | : curMon 161 | ); 162 | setMonitors(newCustMonitors); 163 | console.log('positions to :', initialMonitors.current[focusedMonitorIdx]); 164 | return newCustMonitors; 165 | } 166 | 167 | //ROTATION 168 | const rotationOptions = [ 169 | { value: Rotation.Normal, label: 'Normal' }, 170 | { value: Rotation.Left, label: 'Left' }, 171 | { value: Rotation.Inverted, label: 'Inverted' }, 172 | { value: Rotation.Right, label: 'Right' }, 173 | ]; 174 | function changeRotation(newRotation: Rotation | undefined) { 175 | let prevRotation = customMonitors[focusedMonitorIdx].outputs[0].rotation; 176 | 177 | //correlates to sizes 178 | if (newRotation && newRotation !== prevRotation) { 179 | setMonitors(mons => 180 | mons.map((curMon, idx) => 181 | idx === focusedMonitorIdx 182 | ? { 183 | ...curMon, 184 | outputs: curMon.outputs.map((out, outIdx) => 185 | outIdx === 0 ? { ...out, rotation: newRotation } : out 186 | ), 187 | } 188 | : curMon 189 | ) 190 | ); 191 | } 192 | } 193 | ///This gets called after, needs to ensure position state is kept 194 | function resetRotation(focusedMonitorIdx: number): FrontendMonitor[] { 195 | console.log('reset rotation called'); 196 | console.log(initialMonitors.current); 197 | console.log(customMonitors); 198 | let newCustMonitors = customMonitors.map((curMon, idx) => 199 | idx === focusedMonitorIdx 200 | ? { 201 | ...curMon, 202 | outputs: curMon.outputs.map((out, outIdx) => 203 | outIdx === 0 204 | ? { 205 | ...out, 206 | rotation: initialMonitors.current[focusedMonitorIdx].outputs[0].rotation, 207 | } 208 | : out 209 | ), 210 | } 211 | : { 212 | ...curMon, 213 | } 214 | ); 215 | setMonitors(newCustMonitors); 216 | return newCustMonitors; 217 | } 218 | 219 | //MODE 220 | function setFocusedModeRatio(newRatio: String) { 221 | setMonitors(mons => 222 | mons.map((curMon, idx) => 223 | idx === focusedMonitorIdx 224 | ? { 225 | ...curMon, 226 | outputs: curMon.outputs.map((out, outIdx) => 227 | outIdx === 0 ? { ...out, name: newRatio.toString() } : out 228 | ), 229 | } 230 | : curMon 231 | ) 232 | ); 233 | let futureAvailableModes = initialMonitors.current[focusedMonitorIdx].outputs[0].modes 234 | .filter(mode => mode.name === newRatio) 235 | .sort((a, b) => b.rate - a.rate); 236 | changeModePreset(futureAvailableModes[0]); 237 | } 238 | const modeRatioOptions = [ 239 | ...new Set(initialMonitors.current[focusedMonitorIdx].outputs[0].modes.map(mode => mode.name)), 240 | ].map(uniqueRatios => ({ value: uniqueRatios, label: uniqueRatios })); 241 | const modeFPSOptions = initialMonitors.current[focusedMonitorIdx].outputs[0].modes 242 | .filter(mode => mode.name === customMonitors[focusedMonitorIdx].outputs[0].currentMode!.name) 243 | .sort((a, b) => b.rate - a.rate) 244 | .map(mode => ({ value: mode, label: mode.rate.toFixed(5) })); 245 | function changeModePreset(newVal: Mode | undefined) { 246 | if (newVal && newVal !== customMonitors[focusedMonitorIdx].outputs[0].currentMode) { 247 | setMonitors(mons => 248 | mons.map((curMon, idx) => 249 | idx === focusedMonitorIdx 250 | ? { 251 | ...curMon, 252 | outputs: curMon.outputs.map((out, outIdx) => 253 | outIdx === 0 ? { ...out, currentMode: newVal } : out 254 | ), 255 | } 256 | : curMon 257 | ) 258 | ); 259 | } 260 | } 261 | function resetModePreset(focusedMonitorIdx: number): FrontendMonitor[] { 262 | console.log('mode reset called called'); 263 | console.log(initialMonitors.current); 264 | console.log(customMonitors); 265 | let newCustMonitors = customMonitors.map((curMon, idx) => 266 | idx === focusedMonitorIdx 267 | ? { 268 | ...curMon, 269 | outputs: curMon.outputs.map((out, outIdx) => 270 | outIdx === 0 271 | ? { 272 | ...out, 273 | currentMode: initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode, 274 | } 275 | : out 276 | ), 277 | } 278 | : curMon 279 | ); 280 | setMonitors(newCustMonitors); 281 | return newCustMonitors; 282 | } 283 | 284 | function resetAllFocused() { 285 | console.log('reset was called'); 286 | setMonitors(custMons => 287 | custMons.map((custMon, idx) => 288 | idx === focusedMonitorIdx ? { ...initialMonitors.current[focusedMonitorIdx] } : custMon 289 | ) 290 | ); 291 | } 292 | //Styles 293 | const customStyles = { 294 | control: (base: any) => ({ 295 | ...base, 296 | height: 53, 297 | minHeight: 53, 298 | fontSize: 20, 299 | }), 300 | }; 301 | return ( 302 |
303 |
304 |
305 |

Enabled:

306 |
307 |
308 | 321 |
322 | 329 |
330 |
331 |
332 |

Position:

333 |
334 |
335 |
336 |

X:

337 | setPositionX(Number(eve.target.value))} 343 | /> 344 |
345 |
346 |

Y:

347 | setPositionY(Number(eve.target.value))} 353 | /> 354 |
355 |
356 | 366 |
367 |
368 |
369 |

Rotation:

370 |
371 |
372 |
373 | 385 |
386 |
387 | 397 |
398 |
399 |
400 |

Mode:

401 |
402 |
403 |
404 |

Ratio:

405 | 420 |
421 |
422 |

Rate:

423 | 434 |
435 |
436 | 446 |
447 |
448 | ); 449 | }; 450 | export default FocusedMonitorSettings; 451 | -------------------------------------------------------------------------------- /src/components/FreeHandPosition.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Application, 3 | Container, 4 | ContainerChild, 5 | FederatedPointerEvent, 6 | Graphics, 7 | ICanvas, 8 | BitmapText, 9 | Sprite, 10 | Assets, 11 | } from 'pixi.js'; 12 | import { cloneDeep } from 'lodash'; 13 | 14 | import { useState, useRef, Dispatch, SetStateAction, MutableRefObject, useEffect } from 'react'; 15 | import { FrontendMonitor, point, point as Point, Rotation } from '../globalValues'; 16 | import { convertFileSrc } from '@tauri-apps/api/core'; 17 | import './FreeHandPosition.css'; 18 | 19 | interface FreeHandPositionProps { 20 | initialMonitors: MutableRefObject; 21 | customMonitors: FrontendMonitor[]; 22 | setMonitors: Dispatch>; 23 | rerenderMonitorsContainerRef: MutableRefObject; 24 | normalizePositionsRef: MutableRefObject< 25 | ((customMonitors: FrontendMonitor[]) => FrontendMonitor[]) | null 26 | >; 27 | monitorScale: number; 28 | setMonitorScale: Dispatch>; 29 | setFocusedMonitorIdx: Dispatch>; 30 | } 31 | export const FreeHandPosition: React.FC = ({ 32 | initialMonitors, 33 | customMonitors, 34 | setMonitors: setCustMonitors, 35 | rerenderMonitorsContainerRef, 36 | normalizePositionsRef, 37 | monitorScale, 38 | setMonitorScale, 39 | setFocusedMonitorIdx, 40 | }) => { 41 | const dragTarget = useRef>(null); 42 | const screenDragActive = useRef(false); 43 | const screenDragOffsetTotal = useRef({ x: 0, y: 0 }); 44 | const customMonitorsRef = useRef([...customMonitors]); 45 | const monitorScaleRef = useRef(10); 46 | const initialDragX = useRef(0); 47 | const initialDragY = useRef(0); 48 | const previousMonitorOffset = useRef({ x: 0, y: 0 }); 49 | const previousScreenOffset = useRef({ x: 0, y: 0 }); 50 | const app = useRef(null); 51 | //needs to be use state to update button color 52 | const [snapEnabled, setSnapEnabled] = useState(true); 53 | const snapEnabledRef = useRef(true); 54 | const didInit = useRef(false); 55 | async function init(canvasRef: ICanvas) { 56 | let appLocal = new Application(); 57 | await appLocal.init({ background: '#1a171f', canvas: canvasRef }); 58 | //createGrid(appLocal); 59 | for (let i = 0; i < customMonitors.length; i++) { 60 | appLocal.stage.addChild(await createMonitorContainer(customMonitors[i])); 61 | } 62 | appLocal.resizeTo = window; 63 | appLocal.stage.eventMode = 'static'; 64 | appLocal.stage.hitArea = appLocal.screen; 65 | appLocal.stage.on('rightdown', onScreenDragStart); 66 | appLocal.stage.on('rightup', onScreenDragEnd); 67 | appLocal.stage.on('rightupoutside', onScreenDragEnd); 68 | 69 | appLocal.stage.on('pointerup', onDragEnd); 70 | appLocal.stage.on('pointerupoutside', onDragEnd); 71 | 72 | app.current = appLocal; 73 | } 74 | function updateGlobalPosition(monitorName: string, x: number, y: number) { 75 | //console.log('setting x:', x, 'y:', y); 76 | 77 | customMonitorsRef.current = customMonitorsRef.current.map(curMon => 78 | curMon.name === monitorName ? { ...curMon, y: Math.trunc(y), x: Math.trunc(x) } : curMon 79 | ); 80 | setCustMonitors(customMonitorsRef.current); 81 | } 82 | async function rerenderMonitors(newMonitors: FrontendMonitor[]) { 83 | if (app.current) { 84 | console.log('rerender called'); 85 | customMonitorsRef.current = [...newMonitors]; 86 | //deletion 87 | //app.current!.stage.children.forEach((child) => { child.children.forEach((child) => { child.destroy() }) }); 88 | app.current!.stage.children = []; 89 | //rebuilding 90 | for (let i = 0; i < newMonitors.length; i++) { 91 | app.current!.stage.addChild(await createMonitorContainer(newMonitors[i])); 92 | } 93 | } else { 94 | console.log('app not built yet, unable to rerender'); 95 | } 96 | } 97 | async function createMonitorContainer(monitor: FrontendMonitor): Promise { 98 | // Container 99 | const monitorContainer = new Container(); 100 | monitorContainer.isRenderGroup = true; 101 | monitorContainer.x = monitor.x / monitorScale + screenDragOffsetTotal.current.x; 102 | monitorContainer.y = monitor.y / monitorScale + screenDragOffsetTotal.current.y; 103 | monitorContainer.label = monitor.name; 104 | monitorContainer.cursor = 'pointer'; 105 | monitorScaleRef.current = monitorScale; 106 | const monitorGraphic = new Graphics(); 107 | const monitorText = new BitmapText(); 108 | const textBackgroundGraphic = new Graphics(); 109 | 110 | // handles if the monitor is disabled(should not be seen and interactive) 111 | if (monitor.outputs[0].enabled) { 112 | monitorContainer.eventMode = 'static'; 113 | } else { 114 | monitorContainer.eventMode = 'none'; 115 | monitorContainer.alpha = 0; 116 | } 117 | //square 118 | let monitorWidth = monitor.outputs[0].currentMode!.width / monitorScale; 119 | let monitorHeight = monitor.outputs[0].currentMode!.height / monitorScale; 120 | //handle monitors being sideways 121 | if ( 122 | monitor.outputs[0].rotation === Rotation.Left || 123 | monitor.outputs[0].rotation === Rotation.Right 124 | ) { 125 | monitorWidth = monitor.outputs[0].currentMode!.height / monitorScale; 126 | monitorHeight = monitor.outputs[0].currentMode!.width / monitorScale; 127 | } 128 | 129 | monitorGraphic.rect(0, 0, monitorWidth, monitorHeight); 130 | monitorGraphic.fillStyle = 'black'; 131 | monitorGraphic.fill(); 132 | monitorGraphic.stroke({ width: 2, color: 'pink' }); 133 | //Screenshot 134 | let monitorScreenshotSprite: Sprite | undefined; 135 | if (monitor.imgSrc) { 136 | let path = convertFileSrc(monitor.imgSrc); 137 | let texture = await Assets.load(path); 138 | monitorScreenshotSprite = new Sprite(texture); 139 | 140 | monitorScreenshotSprite.setSize(monitorWidth - 4, monitorHeight - 4); 141 | monitorScreenshotSprite.y = 2; 142 | monitorScreenshotSprite.x = 2; 143 | } 144 | 145 | //text 146 | monitorText.text = monitor.name; 147 | monitorText.tint = 'hotpink'; 148 | switch (monitor.outputs[0].rotation) { 149 | case Rotation.Inverted: 150 | //Inverting 151 | monitorText.x = monitorWidth; 152 | monitorText.y = monitorHeight; 153 | monitorText.rotation = Math.PI; 154 | textBackgroundGraphic.rect( 155 | monitorWidth - monitorText.width, 156 | monitorHeight - monitorText.height, 157 | monitorText.width, 158 | monitorText.height 159 | ); 160 | break; 161 | case Rotation.Right: 162 | //Righting 163 | monitorText.y = monitorHeight - monitorText.width; 164 | monitorText.x = monitorWidth; 165 | monitorText.rotation = Math.PI / 2; 166 | textBackgroundGraphic.rect( 167 | monitorWidth - monitorText.height, 168 | monitorHeight - monitorText.width, 169 | monitorText.height, 170 | monitorText.width 171 | ); 172 | break; 173 | case Rotation.Left: 174 | //Lefting 175 | monitorText.y = monitorHeight; 176 | monitorText.rotation = -(Math.PI / 2); 177 | textBackgroundGraphic.rect( 178 | 0, 179 | monitorHeight - monitorText.width, 180 | monitorText.height, 181 | monitorText.width 182 | ); 183 | break; 184 | default: 185 | textBackgroundGraphic.rect( 186 | monitorText.x, 187 | monitorText.y, 188 | monitorText.width, 189 | monitorText.height 190 | ); 191 | } 192 | textBackgroundGraphic.fillStyle = 'black'; 193 | textBackgroundGraphic.fill(); 194 | 195 | // Setup events for mouse + touch using the pointer events 196 | monitorContainer.on('mousedown', onDragStart, monitorGraphic); 197 | monitorContainer.addChild(monitorGraphic); 198 | if (monitorScreenshotSprite) { 199 | monitorContainer.addChild(monitorScreenshotSprite); 200 | } 201 | monitorContainer.addChild(textBackgroundGraphic); 202 | monitorContainer.addChild(monitorText); 203 | monitorContainer.setSize(monitorWidth, monitorHeight); 204 | return monitorContainer; 205 | } 206 | 207 | function onScreenDragStart(eve: FederatedPointerEvent) { 208 | screenDragActive.current = true; 209 | previousScreenOffset.current.x = eve.globalX; 210 | previousScreenOffset.current.y = eve.globalY; 211 | app.current!.stage.on('pointermove', onScreenMove); 212 | } 213 | 214 | function onScreenMove(eve: FederatedPointerEvent) { 215 | let difX = eve.globalX - previousScreenOffset.current.x; 216 | let difY = eve.globalY - previousScreenOffset.current.y; 217 | previousScreenOffset.current.x = eve.globalX; 218 | previousScreenOffset.current.y = eve.globalY; 219 | screenDragOffsetTotal.current.x += difX; 220 | screenDragOffsetTotal.current.y += difY; 221 | app.current!.stage.children.forEach(child => { 222 | child.x += difX; 223 | child.y += difY; 224 | }); 225 | } 226 | function onScreenDragEnd() { 227 | if (app.current) { 228 | app.current!.stage.off('pointermove', onScreenMove); 229 | } 230 | screenDragActive.current = false; 231 | } 232 | function onDragMove(eve: FederatedPointerEvent) { 233 | let difX = eve.globalX - previousMonitorOffset.current.x; 234 | let difY = eve.globalY - previousMonitorOffset.current.y; 235 | previousMonitorOffset.current.x = eve.globalX; 236 | previousMonitorOffset.current.y = eve.globalY; 237 | if (dragTarget.current) { 238 | dragTarget.current.x += difX; 239 | dragTarget.current.y += difY; 240 | } 241 | } 242 | function onDragStart(eve: FederatedPointerEvent) { 243 | eve.target.alpha = 0.5; 244 | dragTarget.current = eve.target; 245 | setFocusedMonitorIdx(initialMonitors.current.findIndex(mon => mon.name == eve.target.label)); 246 | initialDragX.current = eve.target.x; 247 | initialDragY.current = eve.target.x; 248 | previousMonitorOffset.current.x = eve.globalX; 249 | previousMonitorOffset.current.y = eve.globalY; 250 | 251 | app.current!.stage.on('pointermove', onDragMove); 252 | } 253 | 254 | function convertContainerPoints2MonitorPoints(x: number, y: number): point { 255 | return { 256 | x: (x - screenDragOffsetTotal.current.x) * monitorScaleRef.current, 257 | y: (y - screenDragOffsetTotal.current.y) * monitorScaleRef.current, 258 | }; 259 | } 260 | ///Generates 8 points 261 | function container2Points(monitor: Container): PointAndSource[] { 262 | //scaling it back to monitors to compare against monitors, 263 | // so it can be snapped without loss of converting back and forth 264 | let { x: xScaled, y: yScaled } = convertContainerPoints2MonitorPoints(monitor.x, monitor.y); 265 | let heightScaled = monitor.height * monitorScaleRef.current; 266 | let widthScaled = monitor.width * monitorScaleRef.current; 267 | 268 | //handling redundant math 269 | let middleX = xScaled + widthScaled / 2; 270 | let middleY = yScaled + heightScaled / 2; 271 | let right = xScaled + widthScaled; 272 | let bottom = yScaled + heightScaled; 273 | return [ 274 | // Top left 275 | { monitorName: monitor.label, x: xScaled, y: yScaled, pointRelative: PointRelative.TopLeft }, 276 | { 277 | monitorName: monitor.label, 278 | x: middleX, 279 | y: yScaled, 280 | pointRelative: PointRelative.TopMiddle, 281 | }, 282 | { monitorName: monitor.label, x: right, y: yScaled, pointRelative: PointRelative.TopRight }, 283 | { 284 | monitorName: monitor.label, 285 | x: xScaled, 286 | y: middleY, 287 | pointRelative: PointRelative.MiddleLeft, 288 | }, 289 | { 290 | monitorName: monitor.label, 291 | x: right, 292 | y: middleY, 293 | pointRelative: PointRelative.MiddleRight, 294 | }, 295 | { 296 | monitorName: monitor.label, 297 | x: xScaled, 298 | y: bottom, 299 | pointRelative: PointRelative.BottomLeft, 300 | }, 301 | { 302 | monitorName: monitor.label, 303 | x: middleX, 304 | y: bottom, 305 | pointRelative: PointRelative.BottomMiddle, 306 | }, 307 | { monitorName: monitor.label, x: right, y: bottom, pointRelative: PointRelative.BottomRight }, 308 | ]; 309 | } 310 | enum PointRelative { 311 | TopLeft, 312 | TopMiddle, 313 | TopRight, 314 | MiddleLeft, 315 | MiddleRight, 316 | BottomLeft, 317 | BottomMiddle, 318 | BottomRight, 319 | } 320 | interface PointAndSource { 321 | monitorName: String; 322 | x: number; 323 | y: number; 324 | pointRelative: PointRelative; 325 | } 326 | function monitor2Points(monitor: FrontendMonitor): PointAndSource[] { 327 | //handle rotation 328 | let monitorWidth = monitor.outputs[0].currentMode!.width; 329 | let monitorHeight = monitor.outputs[0].currentMode!.height; 330 | 331 | if ( 332 | monitor.outputs[0].rotation === Rotation.Left || 333 | monitor.outputs[0].rotation === Rotation.Right 334 | ) { 335 | monitorWidth = monitor.outputs[0].currentMode!.height; 336 | monitorHeight = monitor.outputs[0].currentMode!.width; 337 | } 338 | //handle redundant math 339 | let middleX = monitor.x + monitorWidth / 2; 340 | let middleY = monitor.y + monitorHeight / 2; 341 | let right = monitor.x + monitorWidth; 342 | let top = monitor.y; 343 | let bottom = monitor.y + monitorHeight; 344 | let left = monitor.x; 345 | return [ 346 | { monitorName: monitor.name, x: left, y: top, pointRelative: PointRelative.TopLeft }, 347 | { monitorName: monitor.name, x: middleX, y: top, pointRelative: PointRelative.TopMiddle }, 348 | { monitorName: monitor.name, x: right, y: top, pointRelative: PointRelative.TopRight }, 349 | { monitorName: monitor.name, x: left, y: middleY, pointRelative: PointRelative.MiddleLeft }, 350 | { monitorName: monitor.name, x: right, y: middleY, pointRelative: PointRelative.MiddleRight }, 351 | { monitorName: monitor.name, x: left, y: bottom, pointRelative: PointRelative.BottomLeft }, 352 | { 353 | monitorName: monitor.name, 354 | x: middleX, 355 | y: bottom, 356 | pointRelative: PointRelative.BottomMiddle, 357 | }, 358 | { monitorName: monitor.name, x: right, y: bottom, pointRelative: PointRelative.BottomRight }, 359 | ]; 360 | } 361 | 362 | interface pointDiff { 363 | drop: PointAndSource; 364 | target: PointAndSource; 365 | absDifTotal: number; 366 | } 367 | function points2PointDiff( 368 | dropPoints: PointAndSource[], 369 | targetPoints: PointAndSource[] 370 | ): pointDiff[] { 371 | let output: pointDiff[] = []; 372 | for (let dropPointIndex = 0; dropPointIndex < dropPoints.length; dropPointIndex++) { 373 | for (let targetPointIndex = 0; targetPointIndex < targetPoints.length; targetPointIndex++) { 374 | let drop = dropPoints[dropPointIndex]; 375 | let target = targetPoints[targetPointIndex]; 376 | let difX = target.x - drop.x; 377 | let difY = target.y - drop.y; 378 | output.push({ 379 | drop, 380 | target, 381 | absDifTotal: Math.abs(difX) + Math.abs(difY), 382 | }); 383 | } 384 | } 385 | return output; 386 | } 387 | function relative2offset(monitorName: String, relative: PointRelative): point { 388 | let monitor = customMonitorsRef.current.find(mon => mon.name === monitorName); 389 | if (monitor) { 390 | let monitorWidth = monitor.outputs[0].currentMode!.width; 391 | let monitorHeight = monitor.outputs[0].currentMode!.height; 392 | if ( 393 | monitor.outputs[0].rotation === Rotation.Left || 394 | monitor.outputs[0].rotation === Rotation.Right 395 | ) { 396 | monitorWidth = monitor.outputs[0].currentMode!.height; 397 | monitorHeight = monitor.outputs[0].currentMode!.width; 398 | } 399 | //handle redundant math 400 | let middleX = monitorWidth / 2; 401 | let middleY = monitorHeight / 2; 402 | let right = monitorWidth; 403 | let bottom = monitorHeight; 404 | switch (relative) { 405 | case PointRelative.TopMiddle: 406 | return { x: middleX, y: 0 }; 407 | case PointRelative.TopRight: 408 | return { x: right, y: 0 }; 409 | case PointRelative.MiddleLeft: 410 | return { x: 0, y: middleY }; 411 | case PointRelative.MiddleRight: 412 | return { x: right, y: middleY }; 413 | case PointRelative.BottomLeft: 414 | return { x: 0, y: bottom }; 415 | case PointRelative.BottomMiddle: 416 | return { x: middleX, y: bottom }; 417 | case PointRelative.BottomRight: 418 | return { x: right, y: bottom }; 419 | default: 420 | return { x: 0, y: 0 }; 421 | } 422 | } else { 423 | console.log('failed to find monitor during drag drop snap'); 424 | return { x: 0, y: 0 }; 425 | } 426 | } 427 | 428 | function snap(difToSnap: pointDiff) { 429 | if (dragTarget.current) { 430 | let offset = relative2offset(dragTarget.current.label, difToSnap.drop.pointRelative); 431 | let x = difToSnap.target.x - offset.x; 432 | let y = difToSnap.target.y - offset.y; 433 | //convert the part that snapped to the top left 434 | updateGlobalPosition(dragTarget.current.label, x, y); 435 | } else { 436 | console.error('tried to snap on a undefined dragtarget'); 437 | } 438 | } 439 | function onDragEnd() { 440 | if (dragTarget.current && app.current) { 441 | if (snapEnabledRef.current) { 442 | let dropPoints = container2Points(dragTarget.current); 443 | //handle single monitors here 444 | let lowestDif: pointDiff = { 445 | drop: { 446 | monitorName: dragTarget.current.label, 447 | x: dragTarget.current.x * monitorScaleRef.current, 448 | y: dragTarget.current.y * monitorScaleRef.current, 449 | pointRelative: PointRelative.TopLeft, 450 | }, 451 | target: { 452 | monitorName: dragTarget.current.label, 453 | x: dragTarget.current.x * monitorScaleRef.current, 454 | y: dragTarget.current.y * monitorScaleRef.current, 455 | pointRelative: PointRelative.TopLeft, 456 | }, 457 | absDifTotal: Number.MAX_VALUE, 458 | }; 459 | dragTarget.current.label; 460 | for (let i = 0; i < customMonitorsRef.current.length; i++) { 461 | let focusedMonitor = customMonitorsRef.current[i]; 462 | if ( 463 | focusedMonitor.name == dragTarget.current.label || 464 | !focusedMonitor.outputs[0].enabled 465 | ) { 466 | continue; 467 | } 468 | let currentDifCollection = points2PointDiff(dropPoints, monitor2Points(focusedMonitor)); 469 | for (let difIndex = 0; difIndex < currentDifCollection.length; difIndex++) { 470 | if (currentDifCollection[difIndex].absDifTotal < lowestDif.absDifTotal) { 471 | lowestDif = currentDifCollection[difIndex]; 472 | } 473 | } 474 | } 475 | console.log('loweset dif'); 476 | console.log(lowestDif); 477 | snap(lowestDif); 478 | } else { 479 | let { x: xScaled, y: yScaled } = convertContainerPoints2MonitorPoints( 480 | dragTarget.current.x, 481 | dragTarget.current.y 482 | ); 483 | updateGlobalPosition(dragTarget.current.label, xScaled, yScaled); 484 | } 485 | dragTarget.current.alpha = 1; 486 | dragTarget.current = null; 487 | } 488 | app.current!.stage.off('pointermove', onDragMove); 489 | } 490 | function resetCameraPosition() { 491 | if (app.current) { 492 | app.current!.stage.children.forEach(mon => { 493 | mon.x -= screenDragOffsetTotal.current.x; 494 | mon.y -= screenDragOffsetTotal.current.y; 495 | }); 496 | } 497 | screenDragOffsetTotal.current.x = 0; 498 | screenDragOffsetTotal.current.y = 0; 499 | } 500 | function resetMonitorsPositions() { 501 | if (app.current) { 502 | app.current!.stage.children.forEach((mon, idx) => { 503 | mon.x = Math.trunc(initialMonitors.current[idx].x / monitorScale); 504 | mon.y = Math.trunc(initialMonitors.current[idx].y / monitorScale); 505 | }); 506 | } 507 | setCustMonitors(mons => 508 | mons.map((curMon, idx) => ({ 509 | ...curMon, 510 | x: initialMonitors.current[idx].x, 511 | y: initialMonitors.current[idx].y, 512 | })) 513 | ); 514 | screenDragOffsetTotal.current.x = 0; 515 | screenDragOffsetTotal.current.y = 0; 516 | } 517 | function toggleSnap() { 518 | //for rerendero for button 519 | setSnapEnabled(prev => { 520 | return !prev; 521 | }); 522 | //for the listener's ref to understand not to snap anymore 523 | snapEnabledRef.current = !snapEnabledRef.current; 524 | } 525 | ///Used to make it so the farthest left monitor starts at zero x and and lowest monitor to start at zero(adjusting all other monitors accordingly) 526 | function normalizePositions(monitors: FrontendMonitor[]): FrontendMonitor[] { 527 | //console.log("initial XX:", initialMonitors.current[focusedMonitorIdx].x, "| YY:", initialMonitors.current[focusedMonitorIdx].y); 528 | let newMonitors = cloneDeep(monitors); 529 | let minOffsetY = Number.MAX_VALUE; 530 | let minOffsetX = Number.MAX_VALUE; 531 | //find the smallest offsets 532 | newMonitors.forEach(mon => { 533 | if (mon.outputs[0].enabled) { 534 | if (mon.x < minOffsetX) { 535 | minOffsetX = mon.x; 536 | } 537 | if (mon.y < minOffsetY) { 538 | minOffsetY = mon.y; 539 | } 540 | } 541 | }); 542 | 543 | if (minOffsetX == Number.MAX_VALUE && minOffsetY == Number.MAX_VALUE) { 544 | //there are no monitors 545 | console.log('no existing monitors?'); 546 | return monitors; 547 | } 548 | //revert the offset to normalize 549 | if (minOffsetX !== Number.MAX_VALUE) { 550 | console.log('applying offset x:', minOffsetX); 551 | 552 | newMonitors.forEach(mon => { 553 | if (mon.outputs[0].enabled) { 554 | mon.x -= minOffsetX; 555 | } 556 | }); 557 | } 558 | if (minOffsetY !== Number.MAX_VALUE) { 559 | console.log('applying offset x:', minOffsetY); 560 | newMonitors.forEach(mon => { 561 | if (mon.outputs[0].enabled) { 562 | mon.y -= minOffsetY; 563 | } 564 | }); 565 | } 566 | setCustMonitors(newMonitors); 567 | 568 | screenDragOffsetTotal.current.x = 0; 569 | screenDragOffsetTotal.current.y = 0; 570 | return newMonitors; 571 | } 572 | 573 | useEffect(() => { 574 | rerenderMonitorsContainerRef.current = rerenderMonitors; 575 | normalizePositionsRef.current = normalizePositions; 576 | }, [rerenderMonitors, normalizePositions]); 577 | 578 | return ( 579 |
580 | { 589 | if (didInit.current) { 590 | return; 591 | } 592 | init(canvas as any); 593 | didInit.current = true; 594 | }} 595 | onContextMenu={e => { 596 | e.preventDefault(); 597 | }} 598 | > 599 |
600 | 607 | 614 | 621 | 629 |
630 |

Scale

631 |
632 |

1:

633 | { 638 | let newMonitorScale = Number(eve.target.value); 639 | if (newMonitorScale > 0) { 640 | setMonitorScale(newMonitorScale); 641 | } 642 | }} 643 | value={monitorScale} 644 | /> 645 |
646 |
647 |
648 |
649 | ); 650 | }; 651 | export default FreeHandPosition; 652 | -------------------------------------------------------------------------------- /src/components/Popups/ApplySettingsPopup.tsx: -------------------------------------------------------------------------------- 1 | import { MutableRefObject, useEffect, useRef, useState } from 'react'; 2 | import { FrontendMonitor, PositionProps, Rotation } from '../../globalValues'; 3 | import { invoke } from '@tauri-apps/api/core'; 4 | import './ApplySettingsPopup.css'; 5 | import { focusedSettingsFunctions } from '../LoadedScreen'; 6 | import { cloneDeep } from 'lodash'; 7 | interface ApplySettingsPopupProps { 8 | initialMonitors: MutableRefObject; 9 | normalizePositionsRef: MutableRefObject< 10 | ((customMonitors: FrontendMonitor[]) => FrontendMonitor[]) | null 11 | >; 12 | applyChangesRef: MutableRefObject; 13 | resetFunctions: MutableRefObject; 14 | } 15 | interface MonitorApplyState { 16 | overall: AttemptState; 17 | enabled: AttemptState; 18 | position: AttemptState; 19 | rotaiton: AttemptState; 20 | mode: AttemptState; 21 | } 22 | enum AttemptState { 23 | Waiting = 'waiting', 24 | InProgress = 'inProgress', 25 | Unchanged = 'unchanged', 26 | Completed = 'completed', 27 | Failed = 'failed', 28 | Undone = 'undone', 29 | } 30 | interface Attempt { 31 | state: AttemptState; 32 | reason: string; 33 | } 34 | interface FailInfos { 35 | monitorIdx: number; 36 | settingName: string; 37 | reason: string; 38 | } 39 | export const ApplySettingsPopup: React.FC = ({ 40 | applyChangesRef, 41 | initialMonitors, 42 | normalizePositionsRef, 43 | resetFunctions, 44 | }) => { 45 | let defaultMonitorApplyState: MonitorApplyState = { 46 | overall: AttemptState.Waiting, 47 | enabled: AttemptState.Waiting, 48 | position: AttemptState.Waiting, 49 | rotaiton: AttemptState.Waiting, 50 | mode: AttemptState.Waiting, 51 | }; 52 | useEffect(() => { 53 | applyChangesRef.current = applyAllChanges; 54 | }, []); 55 | const failList = useRef([]); 56 | const [showPopup, setShowPopup] = useState(false); 57 | const [monitorStates, setMonitorStates] = useState( 58 | new Array(initialMonitors.current.length).fill({ ...defaultMonitorApplyState }) 59 | ); 60 | const custMonitorsRef = useRef([]); 61 | const [undoButtonText, setUndoButtonText] = useState('...'); 62 | const [nextButtonText, setNextButtonText] = useState('...'); 63 | const [buttonsEnabled, setButtonsEnabled] = useState(false); 64 | const [onErrorScreen, setOnErrorScreen] = useState(true); 65 | const [monitorsBeingChangedState, setMonitorsBeingChangedState] = useState([]); 66 | const undoButtonPressed = useRef(false); 67 | const nextButtonPressed = useRef(false); 68 | 69 | async function applyAllChanges( 70 | customMonitors: FrontendMonitor[], 71 | monitorsBeingAppliedIndexs: number[] 72 | ) { 73 | setNextButtonText('...'); 74 | setOnErrorScreen(false); 75 | failList.current = []; 76 | console.log('Pop up showing'); 77 | setMonitorStates( 78 | new Array(initialMonitors.current.length).fill({ ...defaultMonitorApplyState }) 79 | ); 80 | 81 | custMonitorsRef.current = [...customMonitors]; 82 | monitorsBeingAppliedIndexs.sort( 83 | (m1, m2) => custMonitorsRef.current[m1].x - custMonitorsRef.current[m2].x 84 | ); 85 | setMonitorsBeingChangedState(monitorsBeingAppliedIndexs); 86 | setShowPopup(true); 87 | for (let i = 0; i < monitorsBeingAppliedIndexs.length; i++) { 88 | //set new monitor to in progress along with enable 89 | setMonitorStates(prevMon => 90 | prevMon.map((mon, idx) => 91 | idx === monitorsBeingAppliedIndexs[i] 92 | ? { ...mon, overall: AttemptState.InProgress, enabled: AttemptState.InProgress } 93 | : mon 94 | ) 95 | ); 96 | //enable 97 | // console.log("enabled called on monitor#", i); 98 | // console.log([...instancedMonitors.current]) 99 | let enableAttempt = await applyEnable(monitorsBeingAppliedIndexs[i], custMonitorsRef); 100 | setMonitorStates(prevMon => 101 | prevMon.map((mon, idx) => 102 | idx === monitorsBeingAppliedIndexs[i] ? { ...mon, enabled: enableAttempt.state } : mon 103 | ) 104 | ); 105 | if (enableAttempt.state === AttemptState.Failed) { 106 | failList.current.push({ 107 | monitorIdx: monitorsBeingAppliedIndexs[i], 108 | settingName: 'enabled', 109 | reason: enableAttempt.reason, 110 | }); 111 | } 112 | //disabled or had to force an undo change(happens due to state not wanting to change due to the values === the initial without a change) 113 | if ( 114 | !custMonitorsRef.current[monitorsBeingAppliedIndexs[i]].outputs[0].enabled || 115 | enableAttempt.state !== AttemptState.Undone 116 | ) { 117 | // console.log("position called on monitor#", i); 118 | // console.log([...instancedMonitors.current]) 119 | //position 120 | setMonitorStates(prevMon => 121 | prevMon.map((mon, idx) => 122 | idx === monitorsBeingAppliedIndexs[i] 123 | ? { ...mon, position: AttemptState.InProgress } 124 | : mon 125 | ) 126 | ); 127 | let positionAttempt = await applyPosition( 128 | monitorsBeingAppliedIndexs[i], 129 | custMonitorsRef, 130 | false 131 | ); 132 | setMonitorStates(prevMon => 133 | prevMon.map((mon, idx) => 134 | idx === monitorsBeingAppliedIndexs[i] 135 | ? { ...mon, position: positionAttempt.state } 136 | : mon 137 | ) 138 | ); 139 | if (positionAttempt.state === AttemptState.Failed) { 140 | failList.current.push({ 141 | monitorIdx: monitorsBeingAppliedIndexs[i], 142 | settingName: 'positions', 143 | reason: positionAttempt.reason, 144 | }); 145 | } 146 | // console.log("rotation called on monitor#", i); 147 | // console.log([...instancedMonitors.current]) 148 | // //rotation 149 | setMonitorStates(prevMon => 150 | prevMon.map((mon, idx) => 151 | idx === monitorsBeingAppliedIndexs[i] 152 | ? { ...mon, rotaiton: AttemptState.InProgress } 153 | : mon 154 | ) 155 | ); 156 | let rotationAttempt = await applyRotation( 157 | monitorsBeingAppliedIndexs[i], 158 | custMonitorsRef, 159 | false 160 | ); 161 | setMonitorStates(prevMon => 162 | prevMon.map((mon, idx) => 163 | idx === monitorsBeingAppliedIndexs[i] 164 | ? { ...mon, rotaiton: rotationAttempt.state } 165 | : mon 166 | ) 167 | ); 168 | 169 | if (rotationAttempt.state === AttemptState.Failed) { 170 | failList.current.push({ 171 | monitorIdx: monitorsBeingAppliedIndexs[i], 172 | settingName: 'rotation', 173 | reason: rotationAttempt.reason, 174 | }); 175 | } 176 | // console.log("monitor called on monitor#", i); 177 | // console.log([...instancedMonitors.current]) 178 | // //mode 179 | setMonitorStates(prevMon => 180 | prevMon.map((mon, idx) => 181 | idx === monitorsBeingAppliedIndexs[i] ? { ...mon, mode: AttemptState.InProgress } : mon 182 | ) 183 | ); 184 | let modeAttempt = await applyMode(monitorsBeingAppliedIndexs[i], custMonitorsRef, false); 185 | setMonitorStates(prevMon => 186 | prevMon.map((mon, idx) => 187 | idx === monitorsBeingAppliedIndexs[i] ? { ...mon, mode: modeAttempt.state } : mon 188 | ) 189 | ); 190 | if (modeAttempt.state === AttemptState.Failed) { 191 | failList.current.push({ 192 | monitorIdx: monitorsBeingAppliedIndexs[i], 193 | settingName: 'mode', 194 | reason: modeAttempt.reason, 195 | }); 196 | } 197 | } else { 198 | setMonitorStates(prevMon => 199 | prevMon.map((mon, idx) => 200 | idx === monitorsBeingAppliedIndexs[i] 201 | ? { 202 | ...mon, 203 | position: AttemptState.Completed, 204 | rotation: AttemptState.Completed, 205 | mode: AttemptState.Completed, 206 | } 207 | : mon 208 | ) 209 | ); 210 | } 211 | // console.log("monitor#", i, " finished"); 212 | // console.log([...instancedMonitors.current]) 213 | //overall 214 | if ( 215 | failList.current.findIndex(fail => fail.monitorIdx === monitorsBeingAppliedIndexs[i]) !== -1 216 | ) { 217 | setMonitorStates(prevMon => 218 | prevMon.map((mon, idx) => 219 | idx === monitorsBeingAppliedIndexs[i] ? { ...mon, overall: AttemptState.Failed } : mon 220 | ) 221 | ); 222 | } else { 223 | setMonitorStates(prevMon => 224 | prevMon.map((mon, idx) => 225 | idx === monitorsBeingAppliedIndexs[i] 226 | ? { ...mon, overall: AttemptState.Completed } 227 | : mon 228 | ) 229 | ); 230 | } 231 | } 232 | setOnErrorScreen(true); 233 | } 234 | async function applyEnable( 235 | focusedMonitorIdx: number, 236 | custMonitorsRef: MutableRefObject 237 | ): Promise { 238 | let newMonitorEnabledSetting = custMonitorsRef.current[focusedMonitorIdx].outputs[0].enabled; 239 | let oldMonitorEnabledSetting = initialMonitors.current[focusedMonitorIdx].outputs[0].enabled; 240 | let output: Attempt = { state: AttemptState.Unchanged, reason: '' }; 241 | console.log('apply enabled called on ', focusedMonitorIdx); 242 | if (newMonitorEnabledSetting !== oldMonitorEnabledSetting) { 243 | console.log('enable internal called'); 244 | await invoke('set_enabled', { 245 | xid: custMonitorsRef.current[focusedMonitorIdx].outputs[0].xid, 246 | enabled: newMonitorEnabledSetting, 247 | }) 248 | .then(async newCrtc => { 249 | if (await promptUserToUndo()) { 250 | custMonitorsRef.current = [ 251 | ...resetFunctions.current.enable!(focusedMonitorIdx, oldMonitorEnabledSetting), 252 | ]; 253 | await invoke('set_enabled', { 254 | xid: initialMonitors.current[focusedMonitorIdx].outputs[0].xid, 255 | enabled: oldMonitorEnabledSetting, 256 | }).then(async _newCrtc => { 257 | await applyPosition(focusedMonitorIdx, custMonitorsRef, true); 258 | await applyRotation(focusedMonitorIdx, custMonitorsRef, true); 259 | await applyMode(focusedMonitorIdx, custMonitorsRef, true); 260 | output = { state: AttemptState.Undone, reason: '' }; 261 | }); 262 | } else { 263 | if (!newMonitorEnabledSetting) { 264 | initialMonitors.current[focusedMonitorIdx].outputs[0].enabled = false; 265 | initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.xid = 0; 266 | initialMonitors.current[focusedMonitorIdx].x = 0; 267 | initialMonitors.current[focusedMonitorIdx].y = 0; 268 | initialMonitors.current[focusedMonitorIdx].outputs[0].rotation = Rotation.Normal; 269 | initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.width = 270 | initialMonitors.current[focusedMonitorIdx].widthPx; 271 | initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.height = 272 | initialMonitors.current[focusedMonitorIdx].heightPx; 273 | } else { 274 | /* 275 | crtc.mode = mode.xid; 276 | crtc.width = mode.width; 277 | crtc.height = mode.height; 278 | */ 279 | custMonitorsRef.current = [ 280 | ...resetFunctions.current.setCrtc!(focusedMonitorIdx, newCrtc), 281 | ]; 282 | let prefMode = 283 | initialMonitors.current[focusedMonitorIdx].outputs[0].preferredModes[0]; 284 | initialMonitors.current[focusedMonitorIdx].outputs[0].enabled = true; 285 | initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode = prefMode; 286 | initialMonitors.current[focusedMonitorIdx].widthPx = prefMode.width; 287 | initialMonitors.current[focusedMonitorIdx].heightPx = prefMode.height; 288 | } 289 | output = { state: AttemptState.Completed, reason: '' }; 290 | } 291 | }) 292 | .catch(reason => { 293 | output = { state: AttemptState.Failed, reason: reason }; 294 | }); 295 | } 296 | return output; 297 | } 298 | async function applyRotation( 299 | focusedMonitorIdx: number, 300 | custMonitorsRef: MutableRefObject, 301 | forced: boolean 302 | ): Promise { 303 | let output: Attempt = { state: AttemptState.Unchanged, reason: '' }; 304 | console.log('apply rotation called on ', focusedMonitorIdx); 305 | if ( 306 | forced || 307 | !( 308 | custMonitorsRef.current[focusedMonitorIdx].outputs[0].rotation == 309 | initialMonitors.current[focusedMonitorIdx].outputs[0].rotation 310 | ) 311 | ) { 312 | let newRotation = custMonitorsRef.current[focusedMonitorIdx].outputs[0].rotation; 313 | let oldRotation = initialMonitors.current[focusedMonitorIdx].outputs[0].rotation; 314 | let newWidth = 315 | newRotation === Rotation.Left || newRotation === Rotation.Right 316 | ? initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.height 317 | : initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.width; 318 | let newHeight = 319 | newRotation === Rotation.Left || newRotation === Rotation.Right 320 | ? initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.width 321 | : initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.height; 322 | 323 | console.log('rotation internal called'); 324 | await invoke('set_rotation', { 325 | outputCrtc: custMonitorsRef.current[focusedMonitorIdx].outputs[0].crtc, 326 | rotation: newRotation, 327 | newWidth, 328 | newHeight, 329 | }) 330 | .then(async () => { 331 | if (!forced && (await promptUserToUndo())) { 332 | custMonitorsRef.current = [...resetFunctions.current.rotation!(focusedMonitorIdx)]; 333 | newWidth = 334 | oldRotation === Rotation.Left || oldRotation === Rotation.Right 335 | ? initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.height 336 | : initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.width; 337 | newHeight = 338 | oldRotation === Rotation.Left || oldRotation === Rotation.Right 339 | ? initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.width 340 | : initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode.height; 341 | await invoke('set_rotation', { 342 | outputCrtc: initialMonitors.current[focusedMonitorIdx].outputs[0].crtc, 343 | rotation: oldRotation, 344 | newWidth, 345 | newHeight, 346 | }); 347 | 348 | output = { state: AttemptState.Undone, reason: '' }; 349 | } else { 350 | initialMonitors.current[focusedMonitorIdx].outputs[0].rotation = 351 | custMonitorsRef.current[focusedMonitorIdx].outputs[0].rotation; 352 | output = { state: AttemptState.Completed, reason: '' }; 353 | } 354 | }) 355 | .catch(reason => { 356 | output = { state: AttemptState.Failed, reason: reason }; 357 | }); 358 | } 359 | return output; 360 | } 361 | function monitor2PositionProps(monitors: FrontendMonitor[]): PositionProps[] { 362 | console.log('entered 2positionprops :', monitors); 363 | if (normalizePositionsRef.current) { 364 | monitors = normalizePositionsRef.current!(monitors); 365 | } 366 | return monitors.map(monitor => ({ 367 | output_crtc: monitor.outputs[0].crtc, 368 | x: monitor.x.toFixed(0), 369 | y: monitor.y.toFixed(0), 370 | })); 371 | } 372 | async function applyPosition( 373 | focusedMonitorIdx: number, 374 | custMonitorsRef: MutableRefObject, 375 | forced: boolean 376 | ): Promise { 377 | let output: Attempt = { state: AttemptState.Unchanged, reason: '' }; 378 | console.log('position function called'); 379 | let initialMonitorsClone = cloneDeep(initialMonitors.current); 380 | if ( 381 | forced || 382 | !( 383 | custMonitorsRef.current[focusedMonitorIdx].x === 384 | initialMonitors.current[focusedMonitorIdx].x && 385 | custMonitorsRef.current[focusedMonitorIdx].y === 386 | initialMonitors.current[focusedMonitorIdx].y 387 | ) 388 | ) { 389 | let newPropsList: PositionProps[] = monitor2PositionProps( 390 | initialMonitorsClone.map((mon, posIdx) => 391 | posIdx === focusedMonitorIdx ? custMonitorsRef.current[focusedMonitorIdx] : mon 392 | ) 393 | ); 394 | await invoke('set_positions', { 395 | props: newPropsList, 396 | }) 397 | .then(async () => { 398 | if (!forced && (await promptUserToUndo())) { 399 | initialMonitorsClone = cloneDeep(initialMonitors.current); 400 | let oldPropsList: PositionProps[] = monitor2PositionProps(initialMonitorsClone); 401 | console.log('internals of redo func called'); 402 | custMonitorsRef.current = [...resetFunctions.current.position!(focusedMonitorIdx)]; 403 | console.log( 404 | 'output:', 405 | custMonitorsRef.current[focusedMonitorIdx].outputs[0].crtc, 406 | ',x:', 407 | custMonitorsRef.current[focusedMonitorIdx].x, 408 | ',y:', 409 | custMonitorsRef.current[focusedMonitorIdx].y 410 | ); 411 | await invoke('set_positions', { 412 | props: oldPropsList, 413 | }); 414 | output = { state: AttemptState.Undone, reason: '' }; 415 | custMonitorsRef.current[focusedMonitorIdx].x = 416 | initialMonitors.current[focusedMonitorIdx].x; 417 | custMonitorsRef.current[focusedMonitorIdx].y = 418 | initialMonitors.current[focusedMonitorIdx].y; 419 | } else { 420 | initialMonitors.current[focusedMonitorIdx].x = 421 | custMonitorsRef.current[focusedMonitorIdx].x; 422 | initialMonitors.current[focusedMonitorIdx].y = 423 | custMonitorsRef.current[focusedMonitorIdx].y; 424 | output = { state: AttemptState.Completed, reason: '' }; 425 | } 426 | }) 427 | .catch(reason => { 428 | output = { state: AttemptState.Failed, reason: reason }; 429 | }); 430 | console.log( 431 | 'initial4 x:' + 432 | initialMonitors.current[focusedMonitorIdx].x + 433 | ', initial y:' + 434 | initialMonitors.current[focusedMonitorIdx].y 435 | ); 436 | console.log( 437 | 'cust4 x:' + 438 | custMonitorsRef.current[focusedMonitorIdx].x + 439 | ', cust y:' + 440 | custMonitorsRef.current[focusedMonitorIdx].y 441 | ); 442 | } 443 | return output; 444 | } 445 | async function applyMode( 446 | focusedMonitorIdx: number, 447 | custMonitorsRef: MutableRefObject, 448 | forced: boolean 449 | ): Promise { 450 | let output: Attempt = { state: AttemptState.Unchanged, reason: '' }; 451 | let oldMode = initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode!; 452 | let newMode = custMonitorsRef.current[focusedMonitorIdx].outputs[0].currentMode!; 453 | let newRotation = custMonitorsRef.current[focusedMonitorIdx].outputs[0].rotation; 454 | console.log('apply mode called on ', focusedMonitorIdx); 455 | if (forced || newMode.xid !== oldMode.xid) { 456 | console.log('mode internal called'); 457 | await invoke('set_mode', { 458 | outputCrtc: custMonitorsRef.current[focusedMonitorIdx].outputs[0].crtc, 459 | modeXid: newMode.xid, 460 | modeHeight: 461 | newRotation == Rotation.Left || newRotation == Rotation.Right 462 | ? newMode.width 463 | : newMode.height, 464 | modeWidth: 465 | newRotation == Rotation.Left || newRotation == Rotation.Right 466 | ? newMode.height 467 | : newMode.width, 468 | }) 469 | .then(async () => { 470 | if (!forced && (await promptUserToUndo())) { 471 | await invoke('set_mode', { 472 | outputCrtc: custMonitorsRef.current[focusedMonitorIdx].outputs[0].crtc, 473 | modeXid: oldMode.xid, 474 | modeHeight: 475 | newRotation == Rotation.Left || newRotation == Rotation.Right 476 | ? oldMode.width 477 | : oldMode.height, 478 | modeWidth: 479 | newRotation == Rotation.Left || newRotation == Rotation.Right 480 | ? oldMode.height 481 | : oldMode.width, 482 | }); 483 | custMonitorsRef.current = [...resetFunctions.current.mode!(focusedMonitorIdx)]; 484 | output = { state: AttemptState.Undone, reason: '' }; 485 | } else { 486 | initialMonitors.current[focusedMonitorIdx].outputs[0].currentMode! = 487 | custMonitorsRef.current[focusedMonitorIdx].outputs[0].currentMode!; 488 | output = { state: AttemptState.Completed, reason: '' }; 489 | } 490 | }) 491 | .catch(reason => { 492 | output = { state: AttemptState.Failed, reason: reason }; 493 | }); 494 | } 495 | return output; 496 | } 497 | 498 | //https://stackoverflow.com/questions/37764665/how-to-implement-sleep-function-in-typescript 499 | const secondsToUndo = 15; 500 | async function promptUserToUndo(): Promise { 501 | setButtonsEnabled(true); 502 | undoButtonPressed.current = false; 503 | nextButtonPressed.current = false; 504 | setNextButtonText('Continue'); 505 | for (let i = secondsToUndo; i > -1; i--) { 506 | if (undoButtonPressed.current) { 507 | setUndoButtonText('...'); 508 | setNextButtonText('...'); 509 | return true; 510 | } else if (nextButtonPressed.current) { 511 | setUndoButtonText('...'); 512 | setNextButtonText('...'); 513 | return false; 514 | } else { 515 | setUndoButtonText('Undo(' + i + ')'); 516 | } 517 | await new Promise(resolve => setTimeout(resolve, 1000)); 518 | } 519 | setNextButtonText('...'); 520 | setUndoButtonText('...'); 521 | return false; 522 | } 523 | return onErrorScreen ? ( 524 |
525 |
526 |

Applying Errors

527 |
528 | 529 | 530 | {failList.current.map(err => ( 531 | 532 | 536 | 537 | ))} 538 | 539 |
533 | Failed to apply {err.settingName} on monitor{' '} 534 | {initialMonitors.current[err.monitorIdx].name} because {err.reason} 535 |
540 |
541 | 549 |
550 |
551 | ) : ( 552 |
553 |
554 |

Applying Settings

555 |
556 | {monitorsBeingChangedState.map(monIdx => ( 557 |
558 |

{initialMonitors.current[monIdx].name}

559 |
560 |

Enabled:{monitorStates[monIdx].enabled}

561 |

Position:{monitorStates[monIdx].position}

562 |

Rotation:{monitorStates[monIdx].rotaiton}

563 |

Mode:{monitorStates[monIdx].mode}

564 |
565 | ))} 566 |
567 |
568 | 578 | 588 |
589 |
590 |
591 | ); 592 | }; 593 | export default ApplySettingsPopup; 594 | --------------------------------------------------------------------------------