├── .actrc ├── .cursor └── rules │ ├── project.mdc │ ├── v4-migration.mdc │ └── workflows.mdc ├── .cursorignore ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build-and-package-application.yml │ ├── dependabot-automerge.yml │ ├── lint-application.yml │ ├── lint-general.yml │ └── lint-package-config.yml ├── .gitignore ├── .resources ├── system-bridge-circle-128.png ├── system-bridge-circle-16.png ├── system-bridge-circle-256.png ├── system-bridge-circle-32.png ├── system-bridge-circle-48.png ├── system-bridge-circle-512.png ├── system-bridge-circle-64.png ├── system-bridge-circle.icns ├── system-bridge-circle.ico ├── system-bridge-circle.svg ├── system-bridge-dimmed-128.png ├── system-bridge-dimmed-16.png ├── system-bridge-dimmed-256.png ├── system-bridge-dimmed-32.png ├── system-bridge-dimmed-48.png ├── system-bridge-dimmed-512.png ├── system-bridge-dimmed-64.png ├── system-bridge-dimmed.icns ├── system-bridge-dimmed.ico ├── system-bridge-dimmed.svg ├── system-bridge-rect.png ├── system-bridge-rect.svg ├── system-bridge-win32-installer.bmp ├── system-bridge-win32-installer.png └── system-bridge.rc ├── .scripts ├── linux │ ├── PKGBUILD │ ├── control.template │ ├── create-appimage.sh │ ├── create-arch.sh │ ├── create-deb.sh │ ├── create-flatpak.sh │ ├── create-rpm.sh │ ├── dev.timmo.system-bridge.yml │ ├── system-bridge-backend.sh │ ├── system-bridge.desktop │ └── system-bridge.spec └── windows │ ├── build-sensors.ps1 │ ├── create-installer.ps1 │ └── installer.nsi.template ├── .yamllint.yml ├── LICENSE ├── Makefile ├── README.md ├── backend ├── backend.go ├── http │ ├── api.go │ └── data.go └── websocket │ ├── handlers.go │ ├── instance.go │ ├── messages.go │ └── websocket.go ├── bus └── bus.go ├── data ├── data.go ├── module │ ├── battery.go │ ├── cpu.go │ ├── disks.go │ ├── displays.go │ ├── displays │ │ ├── displays_darwin.go │ │ ├── displays_linux.go │ │ └── displays_windows.go │ ├── gpus.go │ ├── gpus │ │ ├── gpus.go │ │ ├── gpus_darwin.go │ │ ├── gpus_linux.go │ │ └── gpus_windows.go │ ├── media.go │ ├── media │ │ ├── media.go │ │ ├── media_darwin.go │ │ ├── media_linux.go │ │ └── media_windows.go │ ├── memory.go │ ├── networks.go │ ├── networks │ │ ├── connections.go │ │ ├── interfaces.go │ │ ├── statistics.go │ │ └── utils.go │ ├── processes.go │ ├── sensors.go │ ├── sensors │ │ ├── sensors.go │ │ ├── sensors_other.go │ │ └── sensors_windows.go │ └── system.go └── update.go ├── event ├── event.go ├── event_types.go ├── handler │ ├── exit-application.go │ ├── file_info.go │ ├── get-data.go │ ├── get-directories.go │ ├── get-directory.go │ ├── get-file.go │ ├── get-files.go │ ├── get-settings.go │ ├── handler.go │ ├── keyboard-keypress.go │ ├── keyboard-text.go │ ├── media-control.go │ ├── notification.go │ ├── open.go │ ├── power-hibernate.go │ ├── power-lock.go │ ├── power-logout.go │ ├── power-restart.go │ ├── power-shutdown.go │ ├── power-sleep.go │ ├── register-data-listener.go │ ├── system-bridge.autostart.desktop │ ├── types.go │ ├── unregister-data-listener.go │ └── update-settings.go └── response_types.go ├── go.mod ├── go.sum ├── lib └── sensors │ └── windows │ └── .gitignore ├── main.go ├── settings └── settings.go ├── types ├── battery.go ├── cpu.go ├── disks.go ├── displays.go ├── gpus.go ├── media.go ├── memory.go ├── module.go ├── networks.go ├── processes.go ├── sensors.go └── system.go ├── utils ├── handlers │ ├── filesystem │ │ ├── filesystem.go │ │ ├── filesystem_darwin.go │ │ ├── filesystem_linux.go │ │ └── filesystem_windows.go │ ├── keyboard │ │ ├── keyboard.go │ │ ├── keyboard_other.go │ │ └── keyboard_windows.go │ ├── media │ │ ├── media.go │ │ ├── media_darwin.go │ │ ├── media_linux.go │ │ └── media_windows.go │ ├── notification │ │ ├── notification.go │ │ ├── notification_darwin.go │ │ ├── notification_linux.go │ │ └── notification_windows.go │ ├── power │ │ ├── power.go │ │ ├── power_darwin.go │ │ ├── power_linux.go │ │ └── power_windows.go │ └── settings │ │ └── settings.go ├── http │ └── http.go ├── path.go └── token.go ├── version └── version.go └── web-client ├── .gitignore ├── bun.lock ├── components.json ├── eslint.config.js ├── next.config.js ├── package.json ├── postcss.config.js ├── prettier.config.js ├── src ├── app │ ├── (websocket) │ │ ├── (client) │ │ │ ├── data │ │ │ │ ├── _components │ │ │ │ │ └── data.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── settings │ │ │ │ ├── _components │ │ │ │ └── settings.tsx │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── connection │ │ ├── _components │ │ │ └── connection.tsx │ │ └── page.tsx │ ├── icon.tsx │ └── layout.tsx ├── components │ ├── hooks │ │ ├── use-system-bridge-connection.tsx │ │ └── use-system-bridge-ws.tsx │ ├── providers │ │ ├── system-bridge-ws-provider.tsx │ │ └── theme-provider.tsx │ ├── setup-connection.tsx │ ├── theme-toggle.tsx │ └── ui │ │ ├── button-link.tsx │ │ ├── button.tsx │ │ ├── callout.tsx │ │ ├── code-block.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── markdown.tsx │ │ ├── select.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ └── typography.tsx ├── env.js ├── lib │ ├── system-bridge │ │ ├── types-modules.ts │ │ ├── types-settings.ts │ │ └── types-websocket.ts │ └── utils.ts └── styles │ └── globals.css └── tsconfig.json /.actrc: -------------------------------------------------------------------------------- 1 | # Platform mappings 2 | -P ubuntu-latest=catthehacker/ubuntu:act-latest 3 | 4 | # Environment variables 5 | --env CGO_ENABLED=1 6 | 7 | # Secrets (using dummy values for local testing) 8 | --secret ACTIONS_RUNTIME_TOKEN=dummy 9 | 10 | # Other configurations 11 | --artifact-server-path=/tmp/artifacts 12 | -------------------------------------------------------------------------------- /.cursor/rules/project.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | --- 7 | description: 8 | globs: 9 | alwaysApply: true 10 | --- 11 | # Project 12 | 13 | - This project is called System Bridge. 14 | - It supports Windows, Linux, and MacOS (darwin). 15 | - This application is written in @Go 16 | - The aim of this project is to provide a "bridge" to other applications, such as the home automation platform Home Assistant. 17 | - All parts of the application which are exposed outside via the API (such as HTTP and Websockets) should be protected with a token, defined in [settings.go](mdc:settings/settings.go). 18 | - Settings are shared between packages and found in [settings.go](mdc:settings/settings.go). 19 | - The backend and packages it uses and it only are under backend/ 20 | - All data modules are under data/module/* 21 | - All data handlers are under event/handler/* 22 | - Data handlers should contain one function to register and get data. Any functions should be placed in a seperate package at event/handler//* 23 | - The websocket is at [websocket.go](mdc:backend/websocket/websocket.go). Messages are handled in [messages.go](mdc:backend/websocket/messages.go). Its handlers are found at [handlers.go](mdc:backend/websocket/handlers.go). Accessing the websocket internally can be done from using [instance.go](mdc:backend/websocket/instance.go). 24 | - HTTP endpoints are found under backend/http/* 25 | - All shared utility functions are under utils/* 26 | -------------------------------------------------------------------------------- /.cursor/rules/v4-migration.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: false 5 | --- 6 | --- 7 | description: Creating and updating data modules 8 | globs: 9 | alwaysApply: false 10 | --- 11 | 12 | # 4.x.x Migration 13 | 14 | This project was originally python based backend with tauri a rust based desktop framework using react for the frontend. 15 | 16 | The documentation for System Bridge is at @https://system-bridge.timmo.dev/ 17 | 18 | All code for System Bridge can be found on GitHub. 19 | 20 | - The original tauri (rust) application is at @https://github.com/timmo001/system-bridge/tree/4.1.13/* 21 | - The original models / schemas named `systembridgemodels` is at @https://github.com/timmo001/system-bridge-models/* 22 | - The original backend named `systembridgebackend` is at @https://github.com/timmo001/system-bridge-backend/* 23 | - The original CLI named `systembridgecli` is at @https://github.com/timmo001/system-bridge-cli/* 24 | - The original frontend named `systembridgefrontend` is at @https://github.com/timmo001/system-bridge-frontend/* 25 | - The original shared code named `systembridgeshared` is at @https://github.com/timmo001/system-bridge-shared/* 26 | - The original windows sensors named `systembridgewindowssensors` is at @https://github.com/timmo001/system-bridge-windows-sensors/* 27 | - The python connector named `systembridgeconnector` is at @https://github.com/timmo001/system-bridge-connector/* 28 | - This is used in the home assistant implementation 29 | 30 | The Home Assistant implementation is found at @https://github.com/home-assistant/core/tree/dev/homeassistant/components/system_bridge/* 31 | This uses `systembridgeconnector` for its package. When I ask what the original implementation, use this as the source of truth. For example, if I ask what units did this originally use, find the units/unit system/measurement in the Home Assistant implemtation. 32 | When setting up data modules in data/module/ use the units used by home assistant. We want to make sure 5.0.0 is a near replica of 4.x.x and that there are no changes required in the Home Assistant integration. 33 | 34 | Make sure to verify the units in the new implementation match the Home Asssistant implementation/integration units of measurement. If they don't, convert them to match. 35 | -------------------------------------------------------------------------------- /.cursor/rules/workflows.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # GitHub Workflows 7 | 8 | ## Local Testing 9 | 10 | - Use `act` for testing GitHub workflows locally 11 | 12 | ## Usage 13 | 14 | - Run all workflows: `act` 15 | - Run specific workflow: `act -W .github/workflows/workflow-name.yml` 16 | - Run specific job: `act -j job-name` 17 | - List all workflows: `act -l` 18 | 19 | Any commands which rely on docker should be ran with sudo. 20 | 21 | ## Environment 22 | 23 | - Create `.env` file for secrets 24 | - Use `-s GITHUB_TOKEN=your_token` for actions requiring GitHub token 25 | 26 | ## Best Practices 27 | 28 | - Test workflows locally before pushing to remote 29 | - Keep workflow files modular and focused 30 | - Use reusable actions when possible 31 | - Document required secrets and environment variables 32 | - Test both success and failure scenarios 33 | -------------------------------------------------------------------------------- /.cursorignore: -------------------------------------------------------------------------------- 1 | # Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | github: timmo001 3 | ko_fi: timmo001 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "npm" 13 | directory: "/web-client" 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-automerge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Dependabot automerge 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | pull_request_target: {} 7 | 8 | permissions: 9 | pull-requests: write 10 | contents: write 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | dependabot-automerge: 18 | uses: timmo001/workflows/.github/workflows/dependabot-automerge-any.yml@master 19 | -------------------------------------------------------------------------------- /.github/workflows/lint-application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint application 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | types: 11 | - opened 12 | - synchronize 13 | workflow_dispatch: {} 14 | 15 | env: 16 | STATIC_EXPORT: "true" 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | go-lint: 24 | name: Go lint 25 | runs-on: ubuntu-latest 26 | steps: 27 | - name: Checkout code 28 | uses: actions/checkout@v4 29 | 30 | - name: Set up Go 31 | uses: actions/setup-go@v5 32 | with: 33 | cache: true 34 | go-version-file: "go.mod" 35 | 36 | - uses: oven-sh/setup-bun@v2 37 | 38 | - name: Cache bun 39 | uses: actions/cache@v4 40 | with: 41 | path: | 42 | ~/.bun/install/cache 43 | key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} 44 | restore-keys: | 45 | ${{ runner.os }}-bun-cache- 46 | 47 | - name: Install dependencies 48 | run: | 49 | sudo apt-get update 50 | sudo apt-get install -y libx11-dev libxtst-dev libayatana-appindicator3-dev 51 | 52 | - name: Build client project 53 | run: make build_client 54 | 55 | - name: Lint application 56 | uses: golangci/golangci-lint-action@v8 57 | with: 58 | version: latest 59 | -------------------------------------------------------------------------------- /.github/workflows/lint-general.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint general 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | types: 11 | - opened 12 | - synchronize 13 | workflow_dispatch: {} 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | lint-markdown: 21 | name: Lint markdown 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: "22" 31 | 32 | - name: Install dependencies 33 | run: | 34 | sudo npm install -g markdownlint-cli 35 | 36 | - name: Lint markdown 37 | run: markdownlint . 38 | 39 | lint-yaml: 40 | name: Lint yaml 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Checkout code 44 | uses: actions/checkout@v4 45 | 46 | - name: Install dependencies 47 | run: | 48 | sudo apt-get update 49 | sudo apt-get install -y yamllint 50 | 51 | - name: Lint YAML 52 | run: yamllint . 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Builds 2 | *.exe 3 | system-bridge 4 | system-bridge-* 5 | system-bridge.* 6 | !system-bridge*.desktop 7 | !system-bridge*.service 8 | !system-bridge*.spec 9 | 10 | # Legacy 11 | .legacy/ 12 | 13 | # Act - Local GitHub Actions Testing 14 | .act 15 | .secrets 16 | 17 | # OpenCode 18 | .opencode 19 | 20 | # NSIS 21 | !installer.nsi.template 22 | installer.nsi 23 | 24 | # Windows icon 25 | !system-bridge.rc 26 | 27 | # Resources 28 | !.resources/* 29 | 30 | # Build 31 | build/ 32 | -------------------------------------------------------------------------------- /.resources/system-bridge-circle-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-circle-128.png -------------------------------------------------------------------------------- /.resources/system-bridge-circle-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-circle-16.png -------------------------------------------------------------------------------- /.resources/system-bridge-circle-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-circle-256.png -------------------------------------------------------------------------------- /.resources/system-bridge-circle-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-circle-32.png -------------------------------------------------------------------------------- /.resources/system-bridge-circle-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-circle-48.png -------------------------------------------------------------------------------- /.resources/system-bridge-circle-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-circle-512.png -------------------------------------------------------------------------------- /.resources/system-bridge-circle-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-circle-64.png -------------------------------------------------------------------------------- /.resources/system-bridge-circle.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-circle.icns -------------------------------------------------------------------------------- /.resources/system-bridge-circle.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-circle.ico -------------------------------------------------------------------------------- /.resources/system-bridge-dimmed-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-dimmed-128.png -------------------------------------------------------------------------------- /.resources/system-bridge-dimmed-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-dimmed-16.png -------------------------------------------------------------------------------- /.resources/system-bridge-dimmed-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-dimmed-256.png -------------------------------------------------------------------------------- /.resources/system-bridge-dimmed-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-dimmed-32.png -------------------------------------------------------------------------------- /.resources/system-bridge-dimmed-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-dimmed-48.png -------------------------------------------------------------------------------- /.resources/system-bridge-dimmed-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-dimmed-512.png -------------------------------------------------------------------------------- /.resources/system-bridge-dimmed-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-dimmed-64.png -------------------------------------------------------------------------------- /.resources/system-bridge-dimmed.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-dimmed.icns -------------------------------------------------------------------------------- /.resources/system-bridge-dimmed.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-dimmed.ico -------------------------------------------------------------------------------- /.resources/system-bridge-rect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-rect.png -------------------------------------------------------------------------------- /.resources/system-bridge-win32-installer.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-win32-installer.bmp -------------------------------------------------------------------------------- /.resources/system-bridge-win32-installer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timmo001/system-bridge/b3e67f1412fa4e7a03e30acee1775bd4598b7752/.resources/system-bridge-win32-installer.png -------------------------------------------------------------------------------- /.resources/system-bridge.rc: -------------------------------------------------------------------------------- 1 | 1 ICON ".resources/system-bridge-dimmed.ico" 2 | -------------------------------------------------------------------------------- /.scripts/linux/PKGBUILD: -------------------------------------------------------------------------------- 1 | pkgname=system-bridge 2 | pkgver=${ARCH_PKGVER} 3 | pkgrel=1 4 | pkgdesc="A bridge between your systems" 5 | arch=('x86_64') 6 | url="https://github.com/timmo001/system-bridge" 7 | license=('Apache-2.0') 8 | depends=('libx11' 'libxtst' 'libxkbcommon' 'libxkbcommon-x11') 9 | provides=('system-bridge') 10 | conflicts=('system-bridge') 11 | source=('system-bridge' 'system-bridge.desktop' 'system-bridge.svg' 'system-bridge-16.png' 'system-bridge-32.png' 'system-bridge-48.png' 'system-bridge-128.png' 'system-bridge-256.png' 'system-bridge-512.png' 'LICENSE') 12 | 13 | package() { 14 | cd "$srcdir" 15 | 16 | # Create directories 17 | install -dm755 "$pkgdir/usr/bin" 18 | install -dm755 "$pkgdir/usr/share/applications" 19 | install -dm755 "$pkgdir/usr/share/licenses/$pkgname" 20 | install -dm755 "$pkgdir/usr/share/icons/hicolor/scalable/apps" 21 | install -dm755 "$pkgdir/usr/share/icons/hicolor/16x16/apps" 22 | install -dm755 "$pkgdir/usr/share/icons/hicolor/32x32/apps" 23 | install -dm755 "$pkgdir/usr/share/icons/hicolor/48x48/apps" 24 | install -dm755 "$pkgdir/usr/share/icons/hicolor/128x128/apps" 25 | install -dm755 "$pkgdir/usr/share/icons/hicolor/256x256/apps" 26 | install -dm755 "$pkgdir/usr/share/icons/hicolor/512x512/apps" 27 | 28 | # Install binary 29 | install -Dm755 system-bridge "$pkgdir/usr/bin/system-bridge" 30 | 31 | # Install desktop file 32 | install -Dm644 system-bridge.desktop "$pkgdir/usr/share/applications/system-bridge.desktop" 33 | 34 | # Install icons 35 | install -Dm644 system-bridge.svg "$pkgdir/usr/share/icons/hicolor/scalable/apps/system-bridge.svg" 36 | install -Dm644 system-bridge-16.png "$pkgdir/usr/share/icons/hicolor/16x16/apps/system-bridge.png" 37 | install -Dm644 system-bridge-32.png "$pkgdir/usr/share/icons/hicolor/32x32/apps/system-bridge.png" 38 | install -Dm644 system-bridge-48.png "$pkgdir/usr/share/icons/hicolor/48x48/apps/system-bridge.png" 39 | install -Dm644 system-bridge-128.png "$pkgdir/usr/share/icons/hicolor/128x128/apps/system-bridge.png" 40 | install -Dm644 system-bridge-256.png "$pkgdir/usr/share/icons/hicolor/256x256/apps/system-bridge.png" 41 | install -Dm644 system-bridge-512.png "$pkgdir/usr/share/icons/hicolor/512x512/apps/system-bridge.png" 42 | 43 | # Install license 44 | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" 45 | } 46 | -------------------------------------------------------------------------------- /.scripts/linux/control.template: -------------------------------------------------------------------------------- 1 | Package: system-bridge 2 | Version: $VERSION 3 | Section: utils 4 | Priority: optional 5 | Architecture: amd64 6 | Maintainer: Aidan Timson 7 | Homepage: https://github.com/timmo001/system-bridge 8 | Description: System Bridge 9 | A bridge for your systems. 10 | -------------------------------------------------------------------------------- /.scripts/linux/create-appimage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Check if binary exists 6 | if [ ! -f "system-bridge-linux" ]; then 7 | echo "system-bridge-linux not found, please build the application first" 8 | exit 1 9 | fi 10 | 11 | # Setup directories 12 | mkdir -p AppDir/usr/bin 13 | cp system-bridge-linux AppDir/usr/bin/system-bridge 14 | 15 | # Create AppRun script 16 | cat >AppDir/AppRun <new_sums.txt 38 | # Remove the old sha256sums array 39 | sed -i '/^sha256sums=(/,/^)/d' PKGBUILD 40 | # Insert the new sha256sums array after the source= line 41 | awk ' 42 | /source=/ { print; while ((getline line < "new_sums.txt") > 0) print line; next } 43 | { print } 44 | ' PKGBUILD >PKGBUILD.new && mv PKGBUILD.new PKGBUILD 45 | rm new_sums.txt 46 | 47 | # Build package 48 | makepkg -f 49 | 50 | # Move package to dist directory 51 | mkdir -p ../../dist 52 | mv *.pkg.tar.zst ../../dist/ 53 | 54 | cd ../.. 55 | rm -rf build/arch 56 | 57 | # Write the main install file to a text file 58 | echo " 59 | # Install the package 60 | yay -U dist/system-bridge-${ARCH_PKGVER}-1-x86_64.pkg.tar.zst 61 | " 62 | -------------------------------------------------------------------------------- /.scripts/linux/create-deb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Check if binary exists 6 | if [ ! -f "system-bridge-linux" ]; then 7 | echo "system-bridge-linux not found, please build the application first" 8 | exit 1 9 | fi 10 | 11 | VERSION=${VERSION:-5.0.0} 12 | 13 | # Create directory structure 14 | mkdir -p deb-structure/DEBIAN 15 | mkdir -p deb-structure/usr/bin 16 | mkdir -p deb-structure/usr/share/icons/hicolor/scalable/apps 17 | mkdir -p deb-structure/usr/share/icons/hicolor/16x16/apps 18 | mkdir -p deb-structure/usr/share/icons/hicolor/32x32/apps 19 | mkdir -p deb-structure/usr/share/icons/hicolor/48x48/apps 20 | mkdir -p deb-structure/usr/share/icons/hicolor/128x128/apps 21 | mkdir -p deb-structure/usr/share/icons/hicolor/256x256/apps 22 | mkdir -p deb-structure/usr/share/icons/hicolor/512x512/apps 23 | 24 | # Copy binary 25 | cp system-bridge-linux deb-structure/usr/bin/system-bridge 26 | 27 | # Copy icons 28 | cp .resources/system-bridge-dimmed.svg deb-structure/usr/share/icons/hicolor/scalable/apps/system-bridge.svg 29 | cp .resources/system-bridge-dimmed-16.png deb-structure/usr/share/icons/hicolor/16x16/apps/system-bridge.png 30 | cp .resources/system-bridge-dimmed-32.png deb-structure/usr/share/icons/hicolor/32x32/apps/system-bridge.png 31 | cp .resources/system-bridge-dimmed-48.png deb-structure/usr/share/icons/hicolor/48x48/apps/system-bridge.png 32 | cp .resources/system-bridge-dimmed-128.png deb-structure/usr/share/icons/hicolor/128x128/apps/system-bridge.png 33 | cp .resources/system-bridge-dimmed-256.png deb-structure/usr/share/icons/hicolor/256x256/apps/system-bridge.png 34 | cp .resources/system-bridge-dimmed-512.png deb-structure/usr/share/icons/hicolor/512x512/apps/system-bridge.png 35 | 36 | # Create control file from template 37 | sed "s/\$VERSION/$VERSION/g" "$(dirname "$0")/control.template" >deb-structure/DEBIAN/control 38 | 39 | # Build the DEB package 40 | dpkg-deb --build deb-structure dist/system-bridge_${VERSION}_amd64.deb 41 | -------------------------------------------------------------------------------- /.scripts/linux/create-flatpak.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Check if binary exists 6 | if [ ! -f "system-bridge-linux" ]; then 7 | echo "system-bridge-linux not found, please build the application first" 8 | exit 1 9 | fi 10 | 11 | # Check if all icon files exist 12 | for icon in .resources/system-bridge-dimmed.svg \ 13 | .resources/system-bridge-dimmed-16.png \ 14 | .resources/system-bridge-dimmed-32.png \ 15 | .resources/system-bridge-dimmed-48.png \ 16 | .resources/system-bridge-dimmed-128.png \ 17 | .resources/system-bridge-dimmed-256.png \ 18 | .resources/system-bridge-dimmed-512.png; do 19 | if [ ! -f "$icon" ]; then 20 | echo "$icon not found, please add all required icon files" 21 | exit 1 22 | fi 23 | done 24 | 25 | # Required tools check 26 | if ! command -v flatpak-builder &>/dev/null; then 27 | echo "flatpak-builder not found, installing..." 28 | sudo apt-get update 29 | sudo apt-get install -y flatpak flatpak-builder 30 | fi 31 | 32 | sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo 33 | sudo flatpak install -y flathub org.freedesktop.Sdk//23.08 34 | sudo flatpak install -y flathub org.freedesktop.Platform//23.08 35 | 36 | # Create build directory 37 | BUILD_DIR="flatpak-build" 38 | mkdir -p "$BUILD_DIR" 39 | 40 | # Build flatpak package 41 | flatpak-builder --force-clean "$BUILD_DIR" "$(dirname "$0")/dev.timmo.system-bridge.yml" 42 | 43 | # Create repo 44 | mkdir -p repo 45 | flatpak-builder --repo=repo --force-clean "$BUILD_DIR" "$(dirname "$0")/dev.timmo.system-bridge.yml" 46 | 47 | # Create the Flatpak bundle 48 | VERSION=${VERSION:-5.0.0} 49 | mkdir -p dist 50 | flatpak build-bundle repo "dist/system-bridge-${VERSION}.flatpak" dev.timmo.system-bridge 51 | 52 | # Clean up 53 | rm -f "$(dirname "$0")/dev.timmo.system-bridge.yml" 54 | -------------------------------------------------------------------------------- /.scripts/linux/create-rpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Check if binary exists 6 | if [ ! -f "system-bridge-linux" ]; then 7 | echo "system-bridge-linux not found, please build the application first" 8 | exit 1 9 | fi 10 | 11 | VERSION=${VERSION:-5.0.0} 12 | 13 | # Convert version for RPM compatibility 14 | if [[ $VERSION == *"-dev+"* ]]; then 15 | RPM_VERSION="5.0.0" 16 | # Extract the commit hash and use it in the release 17 | COMMIT_HASH=${VERSION#*+} 18 | RPM_RELEASE="0.dev.${COMMIT_HASH}" 19 | else 20 | RPM_VERSION=$VERSION 21 | RPM_RELEASE="1" 22 | fi 23 | 24 | # Create directory structure in rpm-structure 25 | mkdir -p rpm-structure/usr/bin 26 | mkdir -p rpm-structure/usr/share/icons/hicolor/512x512/apps 27 | 28 | # Copy files to rpm-structure 29 | cp system-bridge-linux rpm-structure/usr/bin/system-bridge 30 | 31 | mkdir -p rpm-structure/usr/share/icons/hicolor/scalable/apps 32 | mkdir -p rpm-structure/usr/share/icons/hicolor/16x16/apps 33 | mkdir -p rpm-structure/usr/share/icons/hicolor/32x32/apps 34 | mkdir -p rpm-structure/usr/share/icons/hicolor/48x48/apps 35 | mkdir -p rpm-structure/usr/share/icons/hicolor/128x128/apps 36 | mkdir -p rpm-structure/usr/share/icons/hicolor/256x256/apps 37 | mkdir -p rpm-structure/usr/share/icons/hicolor/512x512/apps 38 | 39 | cp .resources/system-bridge-dimmed.svg rpm-structure/usr/share/icons/hicolor/scalable/apps/system-bridge.svg 40 | cp .resources/system-bridge-dimmed-16.png rpm-structure/usr/share/icons/hicolor/16x16/apps/system-bridge.png 41 | cp .resources/system-bridge-dimmed-32.png rpm-structure/usr/share/icons/hicolor/32x32/apps/system-bridge.png 42 | cp .resources/system-bridge-dimmed-48.png rpm-structure/usr/share/icons/hicolor/48x48/apps/system-bridge.png 43 | cp .resources/system-bridge-dimmed-128.png rpm-structure/usr/share/icons/hicolor/128x128/apps/system-bridge.png 44 | cp .resources/system-bridge-dimmed-256.png rpm-structure/usr/share/icons/hicolor/256x256/apps/system-bridge.png 45 | cp .resources/system-bridge-dimmed-512.png rpm-structure/usr/share/icons/hicolor/512x512/apps/system-bridge.png 46 | 47 | # Create the spec file directory 48 | mkdir -p rpmbuild/SPECS 49 | 50 | # Copy the spec file (with substitutions if needed) 51 | sed -e "s/%{_version}/$RPM_VERSION/g" \ 52 | -e "s/%{_release}/$RPM_RELEASE/g" \ 53 | "$(dirname "$0")/system-bridge.spec" >rpmbuild/SPECS/system-bridge.spec 54 | 55 | # Create BUILDROOT directory structure 56 | BUILDROOT_DIR="rpmbuild/BUILDROOT/system-bridge-${RPM_VERSION}-${RPM_RELEASE}.x86_64" 57 | mkdir -p "${BUILDROOT_DIR}/usr/bin" 58 | mkdir -p "${BUILDROOT_DIR}/usr/share/icons/hicolor/512x512/apps" 59 | 60 | # Copy files to BUILDROOT 61 | cp rpm-structure/usr/bin/system-bridge "${BUILDROOT_DIR}/usr/bin/" 62 | cp rpm-structure/usr/share/icons/hicolor/512x512/apps/system-bridge.png "${BUILDROOT_DIR}/usr/share/icons/hicolor/512x512/apps/" 63 | 64 | # Build the RPM package 65 | rpmbuild --define "_topdir $(pwd)/rpmbuild" \ 66 | --define "_version ${RPM_VERSION}" \ 67 | --define "_release ${RPM_RELEASE}" \ 68 | --define "_builddir $(pwd)/rpm-structure" \ 69 | --define "_rpmdir $(pwd)/dist" \ 70 | -bb rpmbuild/SPECS/system-bridge.spec 71 | -------------------------------------------------------------------------------- /.scripts/linux/dev.timmo.system-bridge.yml: -------------------------------------------------------------------------------- 1 | app-id: dev.timmo.system-bridge 2 | runtime: org.freedesktop.Platform 3 | runtime-version: "23.08" 4 | sdk: org.freedesktop.Sdk 5 | command: system-bridge-backend 6 | finish-args: 7 | - --share=network 8 | - --share=ipc 9 | - --socket=x11 10 | - --socket=wayland 11 | - --device=dri 12 | - --filesystem=host 13 | - --talk-name=org.freedesktop.Notifications 14 | modules: 15 | - name: system-bridge 16 | buildsystem: simple 17 | build-commands: 18 | - install -Dm755 system-bridge-linux /app/bin/system-bridge 19 | - install -Dm755 system-bridge-backend.sh /app/bin/system-bridge-backend 20 | - mkdir -p /app/share/applications 21 | - cp system-bridge.desktop /app/share/applications/dev.timmo.system-bridge.desktop 22 | - sed -i 's|Exec=system-bridge|Exec=system-bridge|g' /app/share/applications/dev.timmo.system-bridge.desktop 23 | - sed -i 's|Icon=system-bridge|Icon=dev.timmo.system-bridge|g' /app/share/applications/dev.timmo.system-bridge.desktop 24 | - mkdir -p /app/share/icons/hicolor/scalable/apps 25 | - mkdir -p /app/share/icons/hicolor/16x16/apps 26 | - mkdir -p /app/share/icons/hicolor/32x32/apps 27 | - mkdir -p /app/share/icons/hicolor/48x48/apps 28 | - mkdir -p /app/share/icons/hicolor/128x128/apps 29 | - mkdir -p /app/share/icons/hicolor/256x256/apps 30 | - mkdir -p /app/share/icons/hicolor/512x512/apps 31 | - cp system-bridge.svg /app/share/icons/hicolor/scalable/apps/dev.timmo.system-bridge.svg 32 | - cp system-bridge-16.png /app/share/icons/hicolor/16x16/apps/dev.timmo.system-bridge.png 33 | - cp system-bridge-32.png /app/share/icons/hicolor/32x32/apps/dev.timmo.system-bridge.png 34 | - cp system-bridge-48.png /app/share/icons/hicolor/48x48/apps/dev.timmo.system-bridge.png 35 | - cp system-bridge-128.png /app/share/icons/hicolor/128x128/apps/dev.timmo.system-bridge.png 36 | - cp system-bridge-256.png /app/share/icons/hicolor/256x256/apps/dev.timmo.system-bridge.png 37 | - cp system-bridge-512.png /app/share/icons/hicolor/512x512/apps/dev.timmo.system-bridge.png 38 | sources: 39 | - type: file 40 | path: ../../system-bridge-linux 41 | - type: file 42 | path: system-bridge-backend.sh 43 | - type: file 44 | path: system-bridge.desktop 45 | - type: file 46 | path: ../../.resources/system-bridge-dimmed.svg 47 | dest-filename: system-bridge.svg 48 | - type: file 49 | path: ../../.resources/system-bridge-dimmed-16.png 50 | dest-filename: system-bridge-16.png 51 | - type: file 52 | path: ../../.resources/system-bridge-dimmed-32.png 53 | dest-filename: system-bridge-32.png 54 | - type: file 55 | path: ../../.resources/system-bridge-dimmed-48.png 56 | dest-filename: system-bridge-48.png 57 | - type: file 58 | path: ../../.resources/system-bridge-dimmed-128.png 59 | dest-filename: system-bridge-128.png 60 | - type: file 61 | path: ../../.resources/system-bridge-dimmed-256.png 62 | dest-filename: system-bridge-256.png 63 | - type: file 64 | path: ../../.resources/system-bridge-dimmed-512.png 65 | dest-filename: system-bridge-512.png 66 | -------------------------------------------------------------------------------- /.scripts/linux/system-bridge-backend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec /app/bin/system-bridge backend "$@" -------------------------------------------------------------------------------- /.scripts/linux/system-bridge.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=System Bridge 3 | Comment=System Bridge 4 | Exec=system-bridge backend --open-web-client 5 | Icon=system-bridge 6 | Type=Application 7 | Categories=Utility; 8 | X-GNOME-Autostart-enabled=true 9 | -------------------------------------------------------------------------------- /.scripts/linux/system-bridge.spec: -------------------------------------------------------------------------------- 1 | Name: system-bridge 2 | Version: %{_version} 3 | Release: %{_release} 4 | Summary: System Bridge 5 | 6 | License: Apache-2.0 7 | 8 | %description 9 | A bridge for your systems. 10 | 11 | %install 12 | mkdir -p %{buildroot}/usr/bin 13 | mkdir -p %{buildroot}/usr/share/icons/hicolor/512x512/apps 14 | cp %{_builddir}/usr/bin/system-bridge %{buildroot}/usr/bin/ 15 | cp %{_builddir}/usr/share/icons/hicolor/512x512/apps/system-bridge.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/ 16 | 17 | %files 18 | /usr/bin/system-bridge 19 | /usr/share/icons/hicolor/512x512/apps/system-bridge.png 20 | -------------------------------------------------------------------------------- /.scripts/windows/build-sensors.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | # Parameters 4 | param( 5 | [string]$Version = "5.0.0" 6 | ) 7 | 8 | # Exit on error 9 | $ErrorActionPreference = "Stop" 10 | 11 | # Ensure version is in semantic version format 12 | if ($Version -notmatch '^\d+\.\d+\.\d+') { 13 | $Version = "5.0.0-$Version" 14 | } 15 | 16 | Write-Host "Building Windows Sensors application..." 17 | 18 | # Create sensors directory if it doesn't exist 19 | $sensorsDir = "lib/sensors/windows" 20 | if (-not (Test-Path $sensorsDir)) { 21 | New-Item -ItemType Directory -Path $sensorsDir -Force | Out-Null 22 | } 23 | 24 | # Create bin directory if it doesn't exist 25 | $binDir = "$sensorsDir/bin" 26 | if (-not (Test-Path $binDir)) { 27 | New-Item -ItemType Directory -Path $binDir -Force | Out-Null 28 | } 29 | 30 | # Get absolute paths 31 | $rootDir = $PWD 32 | $binDirAbs = Join-Path $rootDir $binDir 33 | 34 | # Clone the repository 35 | $repoUrl = "https://github.com/timmo001/system-bridge-windows-sensors.git" 36 | $tempDir = "temp-sensors" 37 | 38 | Write-Host "Cloning Windows Sensors repository..." 39 | if (Test-Path $tempDir) { 40 | Remove-Item -Recurse -Force $tempDir 41 | } 42 | git clone $repoUrl $tempDir 43 | 44 | # Setup .NET 45 | Write-Host "Setting up .NET..." 46 | $dotnetVersion = "8.0" 47 | dotnet --list-sdks 48 | if ($LASTEXITCODE -ne 0) { 49 | Write-Host "Installing .NET SDK..." 50 | winget install Microsoft.DotNet.SDK.$dotnetVersion 51 | if ($LASTEXITCODE -ne 0) { 52 | Write-Error ".NET SDK installation failed" 53 | exit 1 54 | } 55 | } 56 | 57 | # Find the .csproj file 58 | Write-Host "Finding project file..." 59 | $projectFile = Get-ChildItem -Path $tempDir -Filter "*.csproj" -Recurse | Select-Object -First 1 60 | if (-not $projectFile) { 61 | Write-Error "No .NET project file found" 62 | exit 1 63 | } 64 | 65 | # Build the application 66 | Write-Host "Building Windows Sensors application from $($projectFile.FullName)..." 67 | Push-Location $projectFile.Directory 68 | try { 69 | dotnet restore 70 | dotnet publish -c Release -r win-x64 --self-contained true /p:Version=$Version /p:PublishSingleFile=true 71 | 72 | # Copy the built executable 73 | $publishDir = Join-Path $projectFile.Directory "bin/net$dotnetVersion-windows$dotnetVersion/win-x64/publish" 74 | $exePath = Join-Path $publishDir "SystemBridgeWindowsSensors.exe" 75 | 76 | if (-not (Test-Path $exePath)) { 77 | Write-Error "Build output not found at expected path: $exePath" 78 | exit 1 79 | } 80 | 81 | # Copy all files 82 | Copy-Item -Path "$publishDir/*" -Destination $binDirAbs -Recurse -Force 83 | } finally { 84 | Pop-Location 85 | } 86 | 87 | # Cleanup 88 | Remove-Item -Recurse -Force $tempDir 89 | 90 | Write-Host "Windows Sensors build completed successfully" 91 | -------------------------------------------------------------------------------- /.scripts/windows/create-installer.ps1: -------------------------------------------------------------------------------- 1 | # Create NSIS installer script 2 | $version = $env:VERSION 3 | if (-not $version) { 4 | $version = "5.0.0" 5 | } 6 | 7 | # Verify .NET Runtime version 8 | $dotnetVersion = "8.0" 9 | Write-Host "Verifying .NET Runtime $dotnetVersion is available..." 10 | $runtimes = dotnet --list-runtimes 11 | if ($LASTEXITCODE -ne 0 -or -not ($runtimes -match "Microsoft.NETCore.App $dotnetVersion")) { 12 | Write-Warning ".NET $dotnetVersion Runtime not found. The installer will attempt to install it during setup." 13 | } 14 | 15 | # Verify Visual C++ Runtime 16 | Write-Host "Verifying Visual C++ Runtime..." 17 | $vcRuntimePath = "C:\Windows\System32\vcruntime140.dll" 18 | if (-not (Test-Path $vcRuntimePath)) { 19 | Write-Warning "Visual C++ Runtime not found. The installer will attempt to install it during setup." 20 | } 21 | 22 | # List current directory contents for debugging 23 | Write-Host "Current directory contents:" 24 | Get-ChildItem -Path $PWD -Recurse | ForEach-Object { Write-Host $_.FullName } 25 | 26 | # Verify system-bridge.exe exists before building installer 27 | if (-not (Test-Path "dist\system-bridge.exe")) { 28 | Write-Error "system-bridge.exe not found in dist directory" 29 | exit 1 30 | } 31 | 32 | # Verify Windows sensors executable exists 33 | $sensorsExePath = "lib/sensors/windows/bin/SystemBridgeWindowsSensors.exe" 34 | if (-not (Test-Path $sensorsExePath)) { 35 | Write-Error "Windows sensors executable not found at: $sensorsExePath" 36 | exit 1 37 | } 38 | 39 | # Get the script directory 40 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path 41 | 42 | # Read the template and replace version 43 | $templateContent = Get-Content -Path "$scriptDir\installer.nsi.template" -Raw 44 | $installerScript = $templateContent -replace '\$VERSION', $version 45 | 46 | # Write the processed script to a file 47 | Set-Content -Path "installer.nsi" -Value $installerScript 48 | 49 | Write-Host "Building installer with NSIS..." 50 | # Build the installer 51 | & makensis installer.nsi 52 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | line-length: 6 | ignore: | 7 | .github/ 8 | .scripts/ 9 | level: warning 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: build_client 2 | go build -v -ldflags="-X 'github.com/timmo001/system-bridge/version.Version=5.0.0-dev+$(shell git rev-parse --short HEAD)'" -o "system-bridge-linux" . 3 | 4 | build_client: clean-web-client 5 | cd web-client && bun install && STATIC_EXPORT=true bun run build 6 | 7 | create_appimage: 8 | VERSION=5.0.0-dev+$(shell git rev-parse --short HEAD) ./.scripts/linux/create-appimage.sh 9 | 10 | create_arch: 11 | VERSION=5.0.0-dev+$(shell git rev-parse --short HEAD) ./.scripts/linux/create-arch.sh 12 | 13 | create_deb: 14 | VERSION=5.0.0-dev+$(shell git rev-parse --short HEAD) ./.scripts/linux/create-deb.sh 15 | 16 | create_rpm: 17 | VERSION=5.0.0-dev+$(shell git rev-parse --short HEAD) ./.scripts/linux/create-rpm.sh 18 | 19 | install: build 20 | go install . 21 | 22 | run: build 23 | ./system-bridge backend 24 | 25 | clean: clean-web-client 26 | rm -f system-bridge 27 | 28 | clean-web-client: 29 | rm -rf web-client/out 30 | 31 | deps: 32 | go mod tidy 33 | 34 | # Show help 35 | help: 36 | @echo "Available targets:" 37 | @echo " build Build the application" 38 | @echo " build_client Build the web client" 39 | @echo " run Build and run the application (development only)" 40 | @echo " clean Remove build artifacts" 41 | @echo " clean-web-client Remove web client build artifacts" 42 | @echo " deps Install dependencies" 43 | -------------------------------------------------------------------------------- /backend/http/api.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/charmbracelet/log" 8 | ) 9 | 10 | type APIResponse struct { 11 | Status string `json:"status"` 12 | Message string `json:"message"` 13 | Data any `json:"data,omitempty"` 14 | } 15 | 16 | // HandleAPI is the main handler for the /api endpoint 17 | func HandleAPI(w http.ResponseWriter, r *http.Request) { 18 | switch r.Method { 19 | case http.MethodGet: 20 | handleAPIGet(w, r) 21 | default: 22 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 23 | } 24 | } 25 | 26 | func handleAPIGet(w http.ResponseWriter, _ *http.Request) { 27 | log.Info("GET: /api") 28 | 29 | response := APIResponse{ 30 | Status: "success", 31 | Message: "API is running", 32 | Data: map[string]any{}, 33 | } 34 | 35 | w.Header().Set("Content-Type", "application/json") 36 | if err := json.NewEncoder(w).Encode(response); err != nil { 37 | log.Error("Failed to encode response", "error", err) 38 | http.Error(w, "Internal server error", http.StatusInternalServerError) 39 | return 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/http/data.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/charmbracelet/log" 8 | "github.com/timmo001/system-bridge/data" 9 | "github.com/timmo001/system-bridge/settings" 10 | "github.com/timmo001/system-bridge/types" 11 | ) 12 | 13 | // GetModuleDataHandler handles requests to get data for a specific module 14 | func GetModuleDataHandler(settings *settings.Settings, dataStore *data.DataStore) http.HandlerFunc { 15 | return func(w http.ResponseWriter, r *http.Request) { 16 | // Check for API token in both X-API-Token and token headers 17 | token := r.Header.Get("X-API-Token") 18 | if token == "" { 19 | token = r.Header.Get("token") 20 | } 21 | if token != settings.API.Token { 22 | http.Error(w, "Invalid API token", http.StatusUnauthorized) 23 | return 24 | } 25 | 26 | // Get module name from URL path 27 | module := types.ModuleName(r.URL.Path[len("/api/data/"):]) 28 | 29 | // Validate module name 30 | if module == "" { 31 | http.Error(w, "Module name is required", http.StatusBadRequest) 32 | return 33 | } 34 | 35 | // Get module data 36 | m, err := dataStore.GetModule(module) 37 | if err != nil { 38 | log.Info("GET: /api/data/:module", "module", module, "data", "not found") 39 | http.Error(w, "Module not found", http.StatusNotFound) 40 | return 41 | } 42 | 43 | log.Info("GET: /api/data/:module", "module", module) 44 | 45 | // Set response headers 46 | w.Header().Set("Content-Type", "application/json") 47 | 48 | // Write response 49 | if err := json.NewEncoder(w).Encode(m.Data); err != nil { 50 | log.Errorf("Error encoding response: %v", err) 51 | http.Error(w, "Internal server error", http.StatusInternalServerError) 52 | return 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /backend/websocket/instance.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var ( 8 | globalInstance *WebsocketServer 9 | instanceMutex sync.RWMutex 10 | ) 11 | 12 | // GetInstance returns the global WebSocket server instance 13 | func GetInstance() *WebsocketServer { 14 | instanceMutex.RLock() 15 | defer instanceMutex.RUnlock() 16 | return globalInstance 17 | } 18 | 19 | // SetInstance sets the global WebSocket server instance 20 | func SetInstance(instance *WebsocketServer) { 21 | instanceMutex.Lock() 22 | defer instanceMutex.Unlock() 23 | globalInstance = instance 24 | } 25 | -------------------------------------------------------------------------------- /backend/websocket/messages.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/gorilla/websocket" 6 | "github.com/timmo001/system-bridge/event" 7 | ) 8 | 9 | func (ws *WebsocketServer) SendMessage(conn *websocket.Conn, message event.MessageResponse) { 10 | log.Debug("Sending message to connection", "response", message) 11 | 12 | if err := conn.WriteJSON(message); err != nil { 13 | log.Error("Failed to send response:", err) 14 | // If there's an error, remove the connection 15 | if closeErr := conn.Close(); closeErr != nil { 16 | log.Error("Error closing connection:", closeErr) 17 | } 18 | delete(ws.connections, conn.RemoteAddr().String()) 19 | } 20 | } 21 | 22 | func (ws *WebsocketServer) SendError(conn *websocket.Conn, req WebSocketRequest, subtype event.ResponseSubtype, message string) { 23 | response := event.MessageResponse{ 24 | ID: req.ID, 25 | Type: event.ResponseTypeError, 26 | Subtype: subtype, 27 | Data: map[string]string{}, 28 | Message: message, 29 | } 30 | ws.SendMessage(conn, response) 31 | } 32 | -------------------------------------------------------------------------------- /backend/websocket/websocket.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/timmo001/system-bridge/bus" 9 | "github.com/timmo001/system-bridge/data" 10 | "github.com/timmo001/system-bridge/event" 11 | "github.com/timmo001/system-bridge/settings" 12 | "github.com/timmo001/system-bridge/types" 13 | ) 14 | 15 | // WebSocketRequest represents the structure of messages sent over the WebSocket 16 | // Extends event.Message to include a token 17 | type WebSocketRequest struct { 18 | // event.Message fields 19 | ID string `json:"id" mapstructure:"id"` 20 | Event string `json:"event" mapstructure:"event"` 21 | Data any `json:"data" mapstructure:"data"` 22 | // WebSocketRequest fields 23 | Token string `json:"token" mapstructure:"token"` 24 | } 25 | 26 | type WebsocketServer struct { 27 | token string 28 | upgrader websocket.Upgrader 29 | connections map[string]*websocket.Conn 30 | dataListeners map[string][]types.ModuleName 31 | mutex sync.RWMutex 32 | dataStore *data.DataStore 33 | EventRouter *event.MessageRouter 34 | } 35 | 36 | func NewWebsocketServer(settings *settings.Settings, dataStore *data.DataStore, eventRouter *event.MessageRouter) *WebsocketServer { 37 | ws := &WebsocketServer{ 38 | token: settings.API.Token, 39 | connections: make(map[string]*websocket.Conn), 40 | dataListeners: make(map[string][]types.ModuleName), 41 | dataStore: dataStore, 42 | EventRouter: eventRouter, 43 | upgrader: websocket.Upgrader{ 44 | CheckOrigin: func(r *http.Request) bool { 45 | return true // Allow all origins for now 46 | }, 47 | }, 48 | } 49 | SetInstance(ws) 50 | 51 | // Subscribe to module data updates 52 | eb := bus.GetInstance() 53 | eb.Subscribe(bus.EventGetDataModule, "websocket", ws.handleGetDataModule) 54 | eb.Subscribe(bus.EventDataModuleUpdate, "websocket", ws.handleDataModuleUpdate) 55 | 56 | return ws 57 | } 58 | -------------------------------------------------------------------------------- /bus/bus.go: -------------------------------------------------------------------------------- 1 | package bus 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/charmbracelet/log" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | // EventType defines the type of events that can be published 12 | type EventType string 13 | 14 | const ( 15 | // EventGetDataModule is the event type for getting data modules 16 | EventGetDataModule EventType = "GET_DATA_MODULE" 17 | // EventDataModuleUpdate is the event type for data module updates 18 | EventDataModuleUpdate EventType = "DATA_MODULE_UPDATE" 19 | ) 20 | 21 | // Event represents an event in the system 22 | type Event struct { 23 | Type EventType 24 | Data any 25 | } 26 | 27 | // GetDataRequest is the request for getting data modules 28 | type GetDataRequest struct { 29 | Connection string `json:"connection" mapstructure:"connection"` 30 | Modules []types.ModuleName `json:"modules" mapstructure:"modules"` 31 | } 32 | 33 | 34 | // Handler is a function that handles events 35 | type Handler func(event Event) 36 | 37 | // EventBus is a central message broker for the application 38 | type EventBus struct { 39 | subscribers map[EventType]map[string]Handler 40 | mutex sync.RWMutex 41 | } 42 | 43 | // NewEventBus creates a new instance of the event bus 44 | func NewEventBus() *EventBus { 45 | return &EventBus{ 46 | subscribers: make(map[EventType]map[string]Handler), 47 | } 48 | } 49 | 50 | // Subscribe registers a handler for a specific event type 51 | func (eb *EventBus) Subscribe(eventType EventType, subscriberID string, handler Handler) { 52 | eb.mutex.Lock() 53 | defer eb.mutex.Unlock() 54 | 55 | if _, ok := eb.subscribers[eventType]; !ok { 56 | eb.subscribers[eventType] = make(map[string]Handler) 57 | } 58 | 59 | eb.subscribers[eventType][subscriberID] = handler 60 | log.Info(fmt.Sprintf("Subscriber '%s' registered for event type '%s'", subscriberID, eventType)) 61 | } 62 | 63 | // Unsubscribe removes a handler for a specific event type 64 | func (eb *EventBus) Unsubscribe(eventType EventType, subscriberID string) { 65 | eb.mutex.Lock() 66 | defer eb.mutex.Unlock() 67 | 68 | if _, ok := eb.subscribers[eventType]; !ok { 69 | return 70 | } 71 | 72 | if _, ok := eb.subscribers[eventType][subscriberID]; ok { 73 | delete(eb.subscribers[eventType], subscriberID) 74 | log.Info(fmt.Sprintf("Subscriber '%s' unregistered from event type '%s'", subscriberID, eventType)) 75 | } 76 | 77 | // If no subscribers left for this event type, clean up 78 | if len(eb.subscribers[eventType]) == 0 { 79 | delete(eb.subscribers, eventType) 80 | } 81 | } 82 | 83 | // Publish sends an event to all subscribers of the given event type 84 | func (eb *EventBus) Publish(event Event) { 85 | eb.mutex.RLock() 86 | defer eb.mutex.RUnlock() 87 | 88 | if handlers, ok := eb.subscribers[event.Type]; ok { 89 | for id, handler := range handlers { 90 | go func(id string, handler Handler) { 91 | log.Debug(fmt.Sprintf("Delivering event type '%s' to subscriber '%s'", event.Type, id)) 92 | handler(event) 93 | }(id, handler) 94 | } 95 | } 96 | } 97 | 98 | // GetInstance returns the singleton instance of the event bus 99 | var instance *EventBus 100 | var once sync.Once 101 | 102 | func GetInstance() *EventBus { 103 | once.Do(func() { 104 | instance = NewEventBus() 105 | }) 106 | return instance 107 | } 108 | -------------------------------------------------------------------------------- /data/module/battery.go: -------------------------------------------------------------------------------- 1 | package data_module 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/distatus/battery" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | type BatteryModule struct{} 12 | 13 | func (batteryModule BatteryModule) Name() types.ModuleName { return types.ModuleBattery } 14 | func (batteryModule BatteryModule) Update(ctx context.Context) (any, error) { 15 | log.Info("Getting battery data") 16 | 17 | // Get all batteries 18 | batteries, err := battery.GetAll() 19 | // If there's an error getting battery info or no batteries found, return empty data 20 | // This handles both error cases and systems without batteries 21 | if err != nil { 22 | log.Debug("No battery present or error getting battery info") 23 | return types.BatteryData{ 24 | IsCharging: nil, 25 | Percentage: nil, 26 | TimeRemaining: nil, 27 | }, nil 28 | } 29 | 30 | // If no batteries found, return empty data 31 | if len(batteries) == 0 { 32 | log.Debug("No batteries found") 33 | return types.BatteryData{ 34 | IsCharging: nil, 35 | Percentage: nil, 36 | TimeRemaining: nil, 37 | }, nil 38 | } 39 | 40 | // Use the first battery (most systems only have one) 41 | bat := batteries[0] 42 | 43 | // Calculate percentage 44 | percentage := (bat.Current / bat.Full) * 100 45 | 46 | // Determine if charging based on state string 47 | isCharging := bat.State.String() == "Charging" 48 | 49 | // Calculate time remaining (in seconds) 50 | // If charging, use time until full, otherwise use time until empty 51 | var timeRemaining float64 52 | if isCharging { 53 | if bat.ChargeRate > 0 { 54 | timeRemaining = ((bat.Full - bat.Current) / bat.ChargeRate) * 3600 55 | } 56 | } else { 57 | if bat.ChargeRate > 0 { 58 | timeRemaining = (bat.Current / bat.ChargeRate) * 3600 59 | } 60 | } 61 | 62 | return types.BatteryData{ 63 | IsCharging: &isCharging, 64 | Percentage: &percentage, 65 | TimeRemaining: &timeRemaining, 66 | }, nil 67 | } 68 | -------------------------------------------------------------------------------- /data/module/displays.go: -------------------------------------------------------------------------------- 1 | package data_module 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/timmo001/system-bridge/data/module/displays" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | type DisplayModule struct{} 12 | 13 | func (dm DisplayModule) Name() types.ModuleName { return types.ModuleDisplays } 14 | func (dm DisplayModule) Update(ctx context.Context) (any, error) { 15 | log.Info("Getting displays data") 16 | 17 | var displaysData types.DisplaysData 18 | displaysData = make([]types.Display, 0) 19 | 20 | displays, err := displays.GetDisplays() 21 | if err != nil { 22 | log.Error("failed to get display info", "error", err) 23 | return displaysData, err 24 | } 25 | 26 | displaysData = displays 27 | return displaysData, nil 28 | } 29 | -------------------------------------------------------------------------------- /data/module/displays/displays_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package displays 5 | 6 | /* 7 | #cgo LDFLAGS: -framework CoreGraphics -framework CoreFoundation 8 | #include 9 | 10 | CGDirectDisplayID getMainDisplayID() { 11 | return CGMainDisplayID(); 12 | } 13 | 14 | int getActiveDisplays(CGDirectDisplayID *displays, int maxDisplays) { 15 | uint32_t count; 16 | CGGetActiveDisplayList(maxDisplays, displays, &count); 17 | return count; 18 | } 19 | 20 | int getDisplayWidth(CGDirectDisplayID display) { 21 | return (int)CGDisplayPixelsWide(display); 22 | } 23 | 24 | int getDisplayHeight(CGDirectDisplayID display) { 25 | return (int)CGDisplayPixelsHigh(display); 26 | } 27 | 28 | int getDisplayX(CGDirectDisplayID display) { 29 | CGRect bounds = CGDisplayBounds(display); 30 | return (int)bounds.origin.x; 31 | } 32 | 33 | int getDisplayY(CGDirectDisplayID display) { 34 | CGRect bounds = CGDisplayBounds(display); 35 | return (int)bounds.origin.y; 36 | } 37 | 38 | double getDisplayRefreshRate(CGDirectDisplayID display) { 39 | CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display); 40 | double refreshRate = CGDisplayModeGetRefreshRate(mode); 41 | CGDisplayModeRelease(mode); 42 | return refreshRate; 43 | } 44 | 45 | int isDisplayBuiltin(CGDirectDisplayID display) { 46 | return CGDisplayIsBuiltin(display); 47 | } 48 | */ 49 | import "C" 50 | import ( 51 | "fmt" 52 | "unsafe" 53 | 54 | "github.com/charmbracelet/log" 55 | "github.com/timmo001/system-bridge/types" 56 | ) 57 | 58 | func GetDisplays() ([]types.Display, error) { 59 | const maxDisplays = 16 60 | displays := make([]types.Display, 0) 61 | 62 | // Allocate array for display IDs 63 | cDisplays := make([]C.CGDirectDisplayID, maxDisplays) 64 | count := C.getActiveDisplays(&cDisplays[0], maxDisplays) 65 | if count < 1 { 66 | return nil, fmt.Errorf("no displays found") 67 | } 68 | 69 | mainDisplay := C.getMainDisplayID() 70 | 71 | // Convert C array to Go slice 72 | displayIDs := (*[maxDisplays]C.CGDirectDisplayID)(unsafe.Pointer(&cDisplays[0]))[:count:count] 73 | 74 | for _, displayID := range displayIDs { 75 | width := int(C.getDisplayWidth(displayID)) 76 | height := int(C.getDisplayHeight(displayID)) 77 | x := int(C.getDisplayX(displayID)) 78 | y := int(C.getDisplayY(displayID)) 79 | refreshRate := float64(C.getDisplayRefreshRate(displayID)) 80 | isPrimary := displayID == mainDisplay 81 | 82 | display := types.Display{ 83 | ID: fmt.Sprintf("%d", displayID), 84 | Name: fmt.Sprintf("Display %d", displayID), 85 | ResolutionHorizontal: width, 86 | ResolutionVertical: height, 87 | X: x, 88 | Y: y, 89 | RefreshRate: &refreshRate, 90 | IsPrimary: &isPrimary, 91 | } 92 | 93 | if displayID == mainDisplay { 94 | displays = append([]types.Display{display}, displays...) 95 | } else { 96 | displays = append(displays, display) 97 | } 98 | } 99 | 100 | if len(displays) == 0 { 101 | log.Warn("No displays found") 102 | return displays, nil 103 | } 104 | 105 | return displays, nil 106 | } 107 | -------------------------------------------------------------------------------- /data/module/displays/displays_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package displays 5 | 6 | import ( 7 | "fmt" 8 | "regexp" 9 | "syscall" 10 | "unsafe" 11 | 12 | "github.com/charmbracelet/log" 13 | "github.com/timmo001/system-bridge/types" 14 | ) 15 | 16 | var ( 17 | user32 = syscall.NewLazyDLL("user32.dll") 18 | enumDisplayMonitors = user32.NewProc("EnumDisplayMonitors") 19 | getMonitorInfo = user32.NewProc("GetMonitorInfoW") 20 | displayPathRegex = regexp.MustCompile(`^\\\\.\\|^\\\\\?\\`) 21 | displayNameRegex = regexp.MustCompile(`^DISPLAY(\d+)$`) 22 | ) 23 | 24 | type RECT struct { 25 | Left, Top, Right, Bottom int32 26 | } 27 | 28 | type MONITORINFOEX struct { 29 | CbSize uint32 30 | Monitor RECT 31 | WorkArea RECT 32 | Flags uint32 33 | DeviceName [32]uint16 34 | } 35 | 36 | func formatDisplayName(id string) string { 37 | matches := displayNameRegex.FindStringSubmatch(id) 38 | if len(matches) == 2 { 39 | return fmt.Sprintf("Display %s", matches[1]) 40 | } 41 | return id 42 | } 43 | 44 | func GetDisplays() ([]types.Display, error) { 45 | var displays []types.Display 46 | 47 | callback := syscall.NewCallback(func(handle syscall.Handle, dc syscall.Handle, rect *RECT, data uintptr) uintptr { 48 | var info MONITORINFOEX 49 | info.CbSize = uint32(unsafe.Sizeof(info)) 50 | 51 | ret, _, _ := getMonitorInfo.Call( 52 | uintptr(handle), 53 | uintptr(unsafe.Pointer(&info)), 54 | ) 55 | 56 | if ret == 0 { 57 | return 1 58 | } 59 | 60 | width := info.Monitor.Right - info.Monitor.Left 61 | height := info.Monitor.Bottom - info.Monitor.Top 62 | isPrimary := (info.Flags & 0x1) != 0 // MONITORINFOF_PRIMARY 63 | pixelClock := 0.0 64 | refreshRate := 0.0 65 | 66 | displayName := syscall.UTF16ToString(info.DeviceName[:]) 67 | cleanID := displayPathRegex.ReplaceAllString(displayName, "") 68 | display := types.Display{ 69 | ID: cleanID, 70 | Name: formatDisplayName(cleanID), 71 | ResolutionHorizontal: int(width), 72 | ResolutionVertical: int(height), 73 | X: int(info.Monitor.Left), 74 | Y: int(info.Monitor.Top), 75 | IsPrimary: &isPrimary, 76 | PixelClock: &pixelClock, 77 | RefreshRate: &refreshRate, 78 | } 79 | 80 | displays = append(displays, display) 81 | return 1 82 | }) 83 | 84 | ret, _, err := enumDisplayMonitors.Call(0, 0, callback, 0) 85 | if ret == 0 { 86 | return nil, fmt.Errorf("EnumDisplayMonitors failed: %v", err) 87 | } 88 | 89 | if len(displays) == 0 { 90 | log.Warn("No displays found") 91 | return displays, nil 92 | } 93 | 94 | return displays, nil 95 | } 96 | -------------------------------------------------------------------------------- /data/module/gpus.go: -------------------------------------------------------------------------------- 1 | package data_module 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/timmo001/system-bridge/data/module/gpus" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | type GPUModule struct{} 12 | 13 | func (gm GPUModule) Name() types.ModuleName { return types.ModuleGPUs } 14 | func (gm GPUModule) Update(ctx context.Context) (any, error) { 15 | log.Info("Getting GPUs data") 16 | 17 | gpusData := make([]types.GPU, 0) 18 | 19 | gpus, err := gpus.GetGPUs() 20 | if err != nil { 21 | log.Error("failed to get GPU info", "error", err) 22 | return gpusData, err 23 | } 24 | 25 | gpusData = gpus 26 | return gpusData, nil 27 | } 28 | -------------------------------------------------------------------------------- /data/module/gpus/gpus.go: -------------------------------------------------------------------------------- 1 | package gpus 2 | 3 | import ( 4 | "github.com/timmo001/system-bridge/types" 5 | ) 6 | 7 | // GetGPUs returns information about all GPU devices 8 | func GetGPUs() ([]types.GPU, error) { 9 | return getGPUs() 10 | } 11 | -------------------------------------------------------------------------------- /data/module/gpus/gpus_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package gpus 5 | 6 | import ( 7 | "encoding/json" 8 | "os/exec" 9 | 10 | "github.com/charmbracelet/log" 11 | "github.com/timmo001/system-bridge/types" 12 | ) 13 | 14 | func getGPUs() ([]types.GPU, error) { 15 | var gpuList []types.GPU 16 | 17 | // Get GPU information using system_profiler 18 | cmd := exec.Command("system_profiler", "SPDisplaysDataType", "-json") 19 | output, err := cmd.Output() 20 | if err != nil { 21 | log.Error("failed to get GPU info", "error", err) 22 | return gpuList, err 23 | } 24 | 25 | // Parse the JSON output 26 | var result struct { 27 | SPDisplaysDataType []struct { 28 | Name string `json:"_name"` 29 | SPPCICores string `json:"sppci_cores"` 30 | SPPCIModel string `json:"sppci_model"` 31 | SPPCIDeviceType string `json:"sppci_device_type"` 32 | } `json:"SPDisplaysDataType"` 33 | } 34 | 35 | if err := json.Unmarshal(output, &result); err != nil { 36 | log.Error("failed to parse GPU info", "error", err) 37 | return gpuList, err 38 | } 39 | 40 | // Convert to our GPU type 41 | for _, dataType := range result.SPDisplaysDataType { 42 | gpuList = append(gpuList, types.GPU{ 43 | ID: dataType.Name, 44 | Name: dataType.SPPCIModel, 45 | // TODO: add speed metrics 46 | // TODO: add load metrics 47 | // TODO: add memory metrics 48 | // TODO: add power metrics 49 | // TODO: add temp metrics 50 | }) 51 | } 52 | 53 | return gpuList, nil 54 | } 55 | -------------------------------------------------------------------------------- /data/module/gpus/gpus_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package gpus 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/timmo001/system-bridge/types" 13 | ) 14 | 15 | func getGPUs() ([]types.GPU, error) { 16 | var gpuList []types.GPU 17 | 18 | // Try to get NVIDIA GPU info first 19 | cmd := exec.Command("nvidia-smi", "--query-gpu=gpu_name,memory.total,memory.used,memory.free,utilization.gpu,clocks.current.graphics,clocks.current.memory,power.draw,temperature.gpu", "--format=csv,noheader,nounits") 20 | output, err := cmd.Output() 21 | if err == nil { 22 | lines := strings.Split(string(output), "\n") 23 | for _, line := range lines { 24 | fields := strings.Split(line, ",") 25 | if len(fields) >= 9 { 26 | name := strings.TrimSpace(fields[0]) 27 | memoryTotal, _ := strconv.ParseFloat(strings.TrimSpace(fields[1]), 64) 28 | memoryUsed, _ := strconv.ParseFloat(strings.TrimSpace(fields[2]), 64) 29 | memoryFree, _ := strconv.ParseFloat(strings.TrimSpace(fields[3]), 64) 30 | coreLoad, _ := strconv.ParseFloat(strings.TrimSpace(fields[4]), 64) 31 | coreClock, _ := strconv.ParseFloat(strings.TrimSpace(fields[5]), 64) 32 | memoryClock, _ := strconv.ParseFloat(strings.TrimSpace(fields[6]), 64) 33 | powerUsage, _ := strconv.ParseFloat(strings.TrimSpace(fields[7]), 64) 34 | temperature, _ := strconv.ParseFloat(strings.TrimSpace(fields[8]), 64) 35 | 36 | gpuList = append(gpuList, types.GPU{ 37 | ID: fmt.Sprintf("nvidia-%d", len(gpuList)), 38 | Name: name, 39 | CoreClock: &coreClock, 40 | CoreLoad: &coreLoad, 41 | MemoryClock: &memoryClock, 42 | MemoryLoad: &coreLoad, // Use core load as memory load for now 43 | MemoryFree: &memoryFree, 44 | MemoryUsed: &memoryUsed, 45 | MemoryTotal: &memoryTotal, 46 | PowerUsage: &powerUsage, 47 | Temperature: &temperature, 48 | }) 49 | } 50 | } 51 | } 52 | 53 | // If no NVIDIA GPUs found, try to get basic GPU info from lspci 54 | if len(gpuList) == 0 { 55 | cmd = exec.Command("lspci", "-v") 56 | output, err = cmd.Output() 57 | if err == nil { 58 | lines := strings.Split(string(output), "\n") 59 | for _, line := range lines { 60 | if strings.Contains(line, "VGA compatible controller") { 61 | parts := strings.Split(line, ":") 62 | if len(parts) >= 2 { 63 | name := strings.TrimSpace(parts[2]) 64 | gpuList = append(gpuList, types.GPU{ 65 | ID: fmt.Sprintf("gpu-%d", len(gpuList)), 66 | Name: name, 67 | }) 68 | } 69 | } 70 | } 71 | } 72 | } 73 | 74 | return gpuList, nil 75 | } 76 | -------------------------------------------------------------------------------- /data/module/gpus/gpus_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package gpus 5 | 6 | import ( 7 | "encoding/json" 8 | "os/exec" 9 | 10 | "github.com/charmbracelet/log" 11 | "github.com/timmo001/system-bridge/types" 12 | ) 13 | 14 | func getGPUs() ([]types.GPU, error) { 15 | var gpus []types.GPU 16 | 17 | // Get GPU information using PowerShell 18 | cmd := exec.Command("powershell", "-Command", ` 19 | class GPU { 20 | [string]$ID 21 | [string]$Name 22 | [int]$MemoryTotal 23 | } 24 | 25 | $gpus = [System.Collections.Generic.List[GPU]]::new() 26 | Get-WmiObject Win32_VideoController | ForEach-Object { 27 | $gpu = New-Object GPU -Property @{ 28 | ID = $_.DeviceID 29 | Name = $_.Name 30 | MemoryTotal = [math]::Round($_.AdapterRAM / 1GB, 2) 31 | } 32 | $gpus.Add($gpu) 33 | } 34 | ConvertTo-Json -Compress $gpus 35 | `) 36 | 37 | output, err := cmd.Output() 38 | if err != nil { 39 | log.Error("failed to get GPU info", "error", err) 40 | return gpus, err 41 | } 42 | 43 | // Parse the JSON output 44 | var gpuInfo []struct { 45 | ID string `json:"ID"` 46 | Name string `json:"Name"` 47 | MemoryTotal float64 `json:"MemoryTotal"` 48 | } 49 | if err := json.Unmarshal(output, &gpuInfo); err != nil { 50 | log.Error("failed to parse GPU info", "error", err) 51 | return gpus, err 52 | } 53 | 54 | // Convert to our GPU type 55 | for _, info := range gpuInfo { 56 | gpus = append(gpus, types.GPU{ 57 | ID: info.ID, 58 | Name: info.Name, 59 | MemoryTotal: &info.MemoryTotal, 60 | }) 61 | } 62 | 63 | // Get GPU temperature using OpenHardwareMonitor 64 | cmd = exec.Command("powershell", "-Command", ` 65 | Get-WmiObject MSAcpi_ThermalZoneTemperature -Namespace "root/wmi" | ForEach-Object { 66 | $temp = ($_.CurrentTemperature - 2732) / 10.0 67 | [PSCustomObject]@{ 68 | Temperature = $temp 69 | } 70 | } | ConvertTo-Json 71 | `) 72 | 73 | output, err = cmd.Output() 74 | if err == nil { 75 | var temps []struct { 76 | Temperature float64 `json:"Temperature"` 77 | } 78 | if err := json.Unmarshal(output, &temps); err == nil && len(temps) > 0 { 79 | // Assign temperature to the first GPU 80 | if len(gpus) > 0 { 81 | gpus[0].Temperature = &temps[0].Temperature 82 | } 83 | } 84 | } 85 | 86 | return gpus, nil 87 | } 88 | -------------------------------------------------------------------------------- /data/module/media.go: -------------------------------------------------------------------------------- 1 | package data_module 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/timmo001/system-bridge/data/module/media" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | type MediaModule struct{} 12 | 13 | func (mm MediaModule) Name() types.ModuleName { return types.ModuleMedia } 14 | func (mm MediaModule) Update(ctx context.Context) (any, error) { 15 | log.Info("Getting media data") 16 | return media.GetMediaData() 17 | } 18 | -------------------------------------------------------------------------------- /data/module/media/media.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/timmo001/system-bridge/types" 7 | ) 8 | 9 | // GetMediaData gets media information from the system 10 | func GetMediaData() (types.MediaData, error) { 11 | // Get current timestamp 12 | now := float64(time.Now().Unix()) 13 | 14 | // Initialize media data with default values 15 | mediaData := types.MediaData{ 16 | UpdatedAt: &now, 17 | } 18 | 19 | // Get platform-specific media data 20 | return getMediaData(mediaData) 21 | } 22 | -------------------------------------------------------------------------------- /data/module/media/media_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package media 5 | 6 | import ( 7 | "encoding/json" 8 | "os/exec" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/timmo001/system-bridge/types" 13 | ) 14 | 15 | func getMediaData(mediaData types.MediaData) (types.MediaData, error) { 16 | // On Linux, we'll use playerctl to get media information 17 | cmd := exec.Command("playerctl", "metadata", "--format", "json", "--all-players") 18 | output, err := cmd.Output() 19 | if err == nil { 20 | var metadata struct { 21 | Title string `json:"title"` 22 | Artist string `json:"artist"` 23 | Album string `json:"album"` 24 | Duration string `json:"duration"` 25 | Position string `json:"position"` 26 | Status string `json:"status"` 27 | PlayerName string `json:"playerName"` 28 | Volume float64 `json:"volume"` 29 | Shuffle string `json:"shuffle"` 30 | LoopStatus string `json:"loopStatus"` 31 | } 32 | if err := json.Unmarshal(output, &metadata); err == nil { 33 | mediaData.Title = &metadata.Title 34 | mediaData.Artist = &metadata.Artist 35 | mediaData.AlbumTitle = &metadata.Album 36 | mediaData.Status = &metadata.Status 37 | mediaData.Type = &metadata.PlayerName 38 | 39 | // Convert duration and position to float64 40 | if duration, err := strconv.ParseFloat(metadata.Duration, 64); err == nil { 41 | mediaData.Duration = &duration 42 | } 43 | if position, err := strconv.ParseFloat(metadata.Position, 64); err == nil { 44 | mediaData.Position = &position 45 | } 46 | 47 | // Set control states based on status 48 | isPlaying := strings.ToLower(metadata.Status) == "playing" 49 | mediaData.IsPlayEnabled = &[]bool{!isPlaying}[0] 50 | mediaData.IsPauseEnabled = &[]bool{isPlaying}[0] 51 | mediaData.IsStopEnabled = &[]bool{true}[0] 52 | 53 | // Set shuffle and repeat states 54 | if metadata.Shuffle != "" { 55 | isShuffle := strings.ToLower(metadata.Shuffle) == "on" 56 | mediaData.Shuffle = &isShuffle 57 | } 58 | if metadata.LoopStatus != "" { 59 | mediaData.Repeat = &metadata.LoopStatus 60 | } 61 | } 62 | } 63 | 64 | // If playerctl fails or no media is playing, check for browser media 65 | if mediaData.Title == nil { 66 | cmd = exec.Command("xdotool", "search", "--name", "YouTube|Netflix|Spotify|VLC", "getwindowname") 67 | output, err = cmd.Output() 68 | if err == nil { 69 | title := strings.TrimSpace(string(output)) 70 | if title != "" { 71 | mediaData.Title = &title 72 | mediaData.Type = &[]string{"browser"}[0] 73 | mediaData.Status = &[]string{"playing"}[0] 74 | mediaData.IsPlayEnabled = &[]bool{true}[0] 75 | mediaData.IsPauseEnabled = &[]bool{true}[0] 76 | mediaData.IsStopEnabled = &[]bool{true}[0] 77 | } 78 | } 79 | } 80 | 81 | return mediaData, nil 82 | } 83 | -------------------------------------------------------------------------------- /data/module/media/media_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package media 5 | 6 | import ( 7 | "github.com/timmo001/system-bridge/types" 8 | ) 9 | 10 | func getMediaData(mediaData types.MediaData) (types.MediaData, error) { 11 | // TODO: Implement 12 | return mediaData, nil 13 | } 14 | -------------------------------------------------------------------------------- /data/module/memory.go: -------------------------------------------------------------------------------- 1 | package data_module 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/shirou/gopsutil/v4/mem" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | type MemoryModule struct {} 12 | 13 | func (mm MemoryModule) Name() types.ModuleName { return types.ModuleMemory } 14 | func (mm MemoryModule) Update(ctx context.Context) (any, error) { 15 | log.Info("Getting memory data") 16 | 17 | var memoryData types.MemoryData 18 | // Get virtual memory stats 19 | virtualMem, err := mem.VirtualMemory() 20 | if err != nil { 21 | log.Errorf("Failed to get virtual memory: %v", err) 22 | } else { 23 | memoryData.Virtual = &types.MemoryVirtual{ 24 | Total: &virtualMem.Total, 25 | Available: &virtualMem.Available, 26 | Used: &virtualMem.Used, 27 | Free: &virtualMem.Free, 28 | Active: &virtualMem.Active, 29 | Inactive: &virtualMem.Inactive, 30 | Buffers: &virtualMem.Buffers, 31 | Cached: &virtualMem.Cached, 32 | Wired: &virtualMem.Wired, 33 | Shared: &virtualMem.Shared, 34 | Percent: &virtualMem.UsedPercent, 35 | } 36 | } 37 | 38 | // Get swap memory stats 39 | swapMem, err := mem.SwapMemory() 40 | if err != nil { 41 | log.Errorf("Failed to get swap memory: %v", err) 42 | } else { 43 | memoryData.Swap = &types.MemorySwap{ 44 | Total: &swapMem.Total, 45 | Used: &swapMem.Used, 46 | Free: &swapMem.Free, 47 | Percent: &swapMem.UsedPercent, 48 | Sin: &swapMem.Sin, 49 | Sout: &swapMem.Sout, 50 | } 51 | } 52 | 53 | return memoryData, nil 54 | } 55 | -------------------------------------------------------------------------------- /data/module/networks.go: -------------------------------------------------------------------------------- 1 | package data_module 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/timmo001/system-bridge/data/module/networks" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | // implement Updater interface 12 | type NetworkModule struct{} 13 | 14 | func (nm NetworkModule) Name() types.ModuleName { return types.ModuleNetworks } 15 | 16 | // UpdateNetworksModule forwards the call to the networks module 17 | func (nm NetworkModule) Update(ctx context.Context) (any, error) { 18 | log.Info("Getting disks data") 19 | 20 | var networksData types.NetworksData 21 | // Initialize arrays 22 | networksData.Connections = make([]types.NetworkConnection, 0) 23 | networksData.Networks = make([]types.Network, 0) 24 | 25 | networks.GatherConnections(&networksData) 26 | 27 | networks.GatherIOStatistics(&networksData) 28 | 29 | err := networks.GatherInterfaces(&networksData) 30 | if err != nil { 31 | log.Error("Error gathering network interfaces", "error", err) 32 | } 33 | return networksData, nil 34 | } 35 | -------------------------------------------------------------------------------- /data/module/networks/connections.go: -------------------------------------------------------------------------------- 1 | package networks 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/log" 7 | psnet "github.com/shirou/gopsutil/v4/net" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | // gatherConnections collects information about network connections 12 | func GatherConnections(networksData *types.NetworksData) { 13 | // Get network connections 14 | connections, err := psnet.Connections("all") 15 | if err != nil { 16 | log.Warn("Error getting network connections", "error", err) 17 | return 18 | } 19 | 20 | for _, conn := range connections { 21 | fd := int(conn.Fd) 22 | family := int(conn.Family) 23 | connType := int(conn.Type) 24 | 25 | // Format local and remote addresses 26 | laddr := fmt.Sprintf("%s:%d", conn.Laddr.IP, conn.Laddr.Port) 27 | raddr := fmt.Sprintf("%s:%d", conn.Raddr.IP, conn.Raddr.Port) 28 | 29 | pid := int(conn.Pid) 30 | 31 | netConn := types.NetworkConnection{ 32 | FD: &fd, 33 | Family: &family, 34 | Type: &connType, 35 | LAddr: &laddr, 36 | RAddr: &raddr, 37 | Status: &conn.Status, 38 | PID: &pid, 39 | } 40 | 41 | networksData.Connections = append(networksData.Connections, netConn) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /data/module/networks/statistics.go: -------------------------------------------------------------------------------- 1 | package networks 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | psnet "github.com/shirou/gopsutil/v4/net" 6 | "github.com/timmo001/system-bridge/types" 7 | ) 8 | 9 | // gatherIOStatistics collects network I/O statistics 10 | func GatherIOStatistics(networksData *types.NetworksData) { 11 | // Get network I/O stats for all interfaces combined 12 | ioTotal, err := psnet.IOCounters(false) // pernic=false for total stats 13 | if err != nil { 14 | log.Warn("Error getting total network I/O counters", "error", err) 15 | setDefaultIOStats(networksData) 16 | return 17 | } 18 | 19 | if len(ioTotal) == 0 { 20 | setDefaultIOStats(networksData) 21 | return 22 | } 23 | 24 | io := ioTotal[0] // Get the aggregate stats 25 | 26 | bytesSent := int64(io.BytesSent) 27 | bytesRecv := int64(io.BytesRecv) 28 | packetsSent := int64(io.PacketsSent) 29 | packetsRecv := int64(io.PacketsRecv) 30 | errIn := int64(io.Errin) 31 | errOut := int64(io.Errout) 32 | dropIn := int64(io.Dropin) 33 | dropOut := int64(io.Dropout) 34 | 35 | networkIO := types.NetworkIO{ 36 | BytesSent: &bytesSent, 37 | BytesRecv: &bytesRecv, 38 | PacketsSent: &packetsSent, 39 | PacketsRecv: &packetsRecv, 40 | ErrIn: &errIn, 41 | ErrOut: &errOut, 42 | DropIn: &dropIn, 43 | DropOut: &dropOut, 44 | } 45 | 46 | networksData.IO = &networkIO 47 | } 48 | 49 | // setDefaultIOStats sets default (zero) values for I/O statistics 50 | func setDefaultIOStats(networksData *types.NetworksData) { 51 | // Fallback to zeros if unable to get stats 52 | bytesSent := int64(0) 53 | bytesRecv := int64(0) 54 | packetsSent := int64(0) 55 | packetsRecv := int64(0) 56 | errIn := int64(0) 57 | errOut := int64(0) 58 | dropIn := int64(0) 59 | dropOut := int64(0) 60 | 61 | networkIO := types.NetworkIO{ 62 | BytesSent: &bytesSent, 63 | BytesRecv: &bytesRecv, 64 | PacketsSent: &packetsSent, 65 | PacketsRecv: &packetsRecv, 66 | ErrIn: &errIn, 67 | ErrOut: &errOut, 68 | DropIn: &dropIn, 69 | DropOut: &dropOut, 70 | } 71 | 72 | networksData.IO = &networkIO 73 | } 74 | -------------------------------------------------------------------------------- /data/module/networks/utils.go: -------------------------------------------------------------------------------- 1 | package networks 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | // formatNetmask converts a net.IPMask to a human-readable string 9 | func formatNetmask(mask net.IPMask) string { 10 | if len(mask) == 0 { 11 | return "" 12 | } 13 | 14 | // For IPv4 masks, return in dot notation (e.g., 255.255.255.0) 15 | if len(mask) == 4 { 16 | return fmt.Sprintf("%d.%d.%d.%d", mask[0], mask[1], mask[2], mask[3]) 17 | } 18 | 19 | // For IPv6, return the mask length 20 | ones, _ := mask.Size() 21 | return fmt.Sprintf("/%d", ones) 22 | } 23 | 24 | // calculateBroadcast calculates the broadcast address for an IPv4 address and netmask 25 | func calculateBroadcast(ip net.IP, mask net.IPMask) string { 26 | if ip.To4() == nil { 27 | return "" // Not an IPv4 address 28 | } 29 | 30 | broadcast := net.IP(make([]byte, 4)) 31 | for i := range 4 { 32 | broadcast[i] = ip.To4()[i] | ^mask[i] 33 | } 34 | return broadcast.String() 35 | } 36 | -------------------------------------------------------------------------------- /data/module/processes.go: -------------------------------------------------------------------------------- 1 | package data_module 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/shirou/gopsutil/v4/process" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | type ProcessModule struct{} 12 | 13 | func (pm ProcessModule) Name() types.ModuleName { return types.ModuleProcesses } 14 | func (pm ProcessModule) Update(ctx context.Context) (any, error) { 15 | log.Info("Getting processes data") 16 | 17 | processesData := make([]types.Process, 0) 18 | 19 | // Get process list 20 | processes, err := process.Processes() 21 | if err != nil { 22 | log.Errorf("Failed to get processes: %v", err) 23 | return processesData, err 24 | } 25 | 26 | // Process each process 27 | for _, p := range processes { 28 | proc := types.Process{ 29 | ID: float64(p.Pid), 30 | } 31 | 32 | // Get process name 33 | name, err := p.Name() 34 | if err == nil { 35 | proc.Name = &name 36 | } 37 | 38 | // Get CPU usage percentage 39 | cpuPercent, err := p.CPUPercent() 40 | if err == nil { 41 | proc.CPUUsage = &cpuPercent 42 | } 43 | 44 | // Get creation time 45 | createTime, err := p.CreateTime() 46 | if err == nil { 47 | createTimeFloat := float64(createTime) / 1000.0 48 | proc.Created = &createTimeFloat 49 | } 50 | 51 | // Get memory usage 52 | memInfo, err := p.MemoryPercent() 53 | if err == nil { 54 | memPercent := float64(memInfo) / 100.0 // Convert to decimal percentage 55 | proc.MemoryUsage = &memPercent 56 | } 57 | 58 | // Get executable path 59 | exePath, err := p.Exe() 60 | if err == nil { 61 | proc.Path = &exePath 62 | } 63 | 64 | // Get process status 65 | statusSlice, err := p.Status() 66 | if err == nil && len(statusSlice) > 0 { 67 | status := statusSlice[0] 68 | proc.Status = &status 69 | } 70 | 71 | // Get username 72 | username, err := p.Username() 73 | if err == nil { 74 | proc.Username = &username 75 | } 76 | 77 | // Get working directory 78 | cwd, err := p.Cwd() 79 | if err == nil { 80 | proc.WorkingDirectory = &cwd 81 | } 82 | 83 | // Add process to data 84 | processesData = append(processesData, proc) 85 | } 86 | 87 | return processesData, nil 88 | } 89 | -------------------------------------------------------------------------------- /data/module/sensors.go: -------------------------------------------------------------------------------- 1 | package data_module 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/timmo001/system-bridge/data/module/sensors" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | type SensorModule struct{} 12 | 13 | func (sm SensorModule) Name() types.ModuleName { return types.ModuleSensors } 14 | func (sm SensorModule) Update(ctx context.Context) (any, error) { 15 | 16 | windowsSensors, err := sensors.GetWindowsSensorsData() 17 | if err != nil { 18 | log.Error("Could not fetch Windows sensor data", "err", err) 19 | } 20 | temperatures, err := sensors.GetTemperatureSensorsData() 21 | if err != nil { 22 | log.Error("Could not fetch temperature sensor data", "err", err) 23 | } 24 | 25 | return types.SensorsData{ 26 | WindowsSensors: windowsSensors, 27 | Temperatures: temperatures, 28 | Fans: nil, 29 | }, nil 30 | } 31 | -------------------------------------------------------------------------------- /data/module/sensors/sensors.go: -------------------------------------------------------------------------------- 1 | package sensors 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/shirou/gopsutil/v4/sensors" 6 | "github.com/timmo001/system-bridge/types" 7 | ) 8 | 9 | func GetTemperatureSensorsData() ([]types.Temperature, error) { 10 | temperatures := make([]types.Temperature, 0) 11 | temperatureStats, err := sensors.SensorsTemperatures() 12 | if err != nil { 13 | log.Error("failed to get temperature stats", "error", err) 14 | return temperatures, err 15 | } else { 16 | for _, ts := range temperatureStats { 17 | temperatures = append(temperatures, types.Temperature{SensorKey: ts.SensorKey, Temperature: ts.Temperature, High: ts.High, Critical: ts.Critical}) 18 | } 19 | } 20 | return temperatures, nil 21 | } 22 | -------------------------------------------------------------------------------- /data/module/sensors/sensors_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package sensors 5 | 6 | import ( 7 | "github.com/timmo001/system-bridge/types" 8 | ) 9 | 10 | func GetWindowsSensorsData() (*types.SensorsWindows, error) { 11 | var sensorsData types.SensorsWindows 12 | sensorsData.Hardware = make([]types.SensorsWindowsHardware, 0) 13 | sensorsData.NVIDIA = &types.SensorsNVIDIA{ 14 | Displays: make([]types.SensorsNVIDIADisplay, 0), 15 | GPUs: make([]types.SensorsNVIDIAGPU, 0), 16 | } 17 | 18 | return &sensorsData, nil 19 | } 20 | -------------------------------------------------------------------------------- /data/module/sensors/sensors_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package sensors 5 | 6 | import ( 7 | "encoding/json" 8 | "os" 9 | "os/exec" 10 | "syscall" 11 | 12 | "github.com/charmbracelet/log" 13 | "github.com/timmo001/system-bridge/types" 14 | ) 15 | 16 | // GetWindowsSensorsData fetches sensor data available only on Windows platforms 17 | func GetWindowsSensorsData() (*types.SensorsWindows, error) { 18 | var windowsSensors types.SensorsWindows 19 | windowsSensors.Hardware = make([]types.SensorsWindowsHardware, 0) 20 | windowsSensors.NVIDIA = &types.SensorsNVIDIA{ 21 | Displays: make([]types.SensorsNVIDIADisplay, 0), 22 | GPUs: make([]types.SensorsNVIDIAGPU, 0), 23 | } 24 | 25 | // Run lib/sensors/windows/bin/SystemBridgeWindowsSensors.exe if it exists 26 | if _, err := os.Stat("lib/sensors/windows/bin/SystemBridgeWindowsSensors.exe"); err == nil { 27 | cmd := exec.Command("lib/sensors/windows/bin/SystemBridgeWindowsSensors.exe") 28 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 29 | output, err := cmd.Output() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | log.Debug("Windows sensors data", "data", string(output)) 35 | 36 | // Parse the output 37 | json.Unmarshal(output, &windowsSensors) 38 | } 39 | 40 | return &windowsSensors, nil 41 | } 42 | -------------------------------------------------------------------------------- /event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/timmo001/system-bridge/settings" 6 | "github.com/timmo001/system-bridge/types" 7 | ) 8 | 9 | // Message is the base type for all events 10 | type Message struct { 11 | ID string `json:"id" mapstructure:"id"` 12 | Event EventType `json:"event" mapstructure:"event"` 13 | Data any `json:"data" mapstructure:"data"` 14 | } 15 | 16 | // MessageResponse is the base type for all responses 17 | type MessageResponse struct { 18 | ID string `json:"id" mapstructure:"id"` 19 | Type ResponseType `json:"type" mapstructure:"type"` 20 | Subtype ResponseSubtype `json:"subtype" mapstructure:"subtype"` 21 | Data any `json:"data" mapstructure:"data"` 22 | Message string `json:"message,omitempty" mapstructure:"message,omitempty"` 23 | Module types.ModuleName `json:"module,omitempty" mapstructure:"module,omitempty"` 24 | } 25 | 26 | // MessageHandler is the type for all event handlers 27 | type MessageHandler func(connection string, message Message) MessageResponse 28 | 29 | // MessageRouter is the type for all event routers 30 | type MessageRouter struct { 31 | Settings *settings.Settings 32 | Handlers map[EventType]MessageHandler 33 | } 34 | 35 | // NewMessageRouter creates a new MessageRouter 36 | func NewMessageRouter(settings *settings.Settings) *MessageRouter { 37 | return &MessageRouter{ 38 | Settings: settings, 39 | Handlers: make(map[EventType]MessageHandler), 40 | } 41 | } 42 | 43 | // RegisterHandler registers a new event handler 44 | func (mr *MessageRouter) RegisterHandler(event EventType, handler MessageHandler) { 45 | log.Info("Registering event handler", "event", event) 46 | mr.Handlers[event] = handler 47 | } 48 | 49 | // RegisterSimpleHandler registers a new event handler 50 | func (mr *MessageRouter) RegisterSimpleHandler(event EventType, fn func(string, Message) MessageResponse) { 51 | mr.RegisterHandler(event, MessageHandler(fn)) 52 | } 53 | 54 | // HandleMessage handles a new event 55 | func (mr *MessageRouter) HandleMessage(connection string, message Message) MessageResponse { 56 | if handler, ok := mr.Handlers[message.Event]; ok { 57 | return handler(connection, message) 58 | } 59 | 60 | log.Warn("Method not found", "event", message.Event) 61 | 62 | return MessageResponse{ 63 | ID: message.ID, 64 | Type: ResponseTypeError, 65 | Data: map[string]Message{ 66 | "message": message, 67 | }, 68 | Message: "Method not found", 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /event/event_types.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | // EventType represents the type of event being handled 4 | type EventType string 5 | 6 | const ( 7 | EventExitApplication EventType = "EXIT_APPLICATION" 8 | EventGetData EventType = "GET_DATA" 9 | EventGetDirectories EventType = "GET_DIRECTORIES" 10 | EventGetDirectory EventType = "GET_DIRECTORY" 11 | EventGetFiles EventType = "GET_FILES" 12 | EventGetFile EventType = "GET_FILE" 13 | EventGetSettings EventType = "GET_SETTINGS" 14 | EventKeyboardKeypress EventType = "KEYBOARD_KEYPRESS" 15 | EventKeyboardText EventType = "KEYBOARD_TEXT" 16 | EventMediaControl EventType = "MEDIA_CONTROL" 17 | EventNotification EventType = "NOTIFICATION" 18 | EventOpen EventType = "OPEN" 19 | EventPowerHibernate EventType = "POWER_HIBERNATE" 20 | EventPowerLock EventType = "POWER_LOCK" 21 | EventPowerLogout EventType = "POWER_LOGOUT" 22 | EventPowerRestart EventType = "POWER_RESTART" 23 | EventPowerShutdown EventType = "POWER_SHUTDOWN" 24 | EventPowerSleep EventType = "POWER_SLEEP" 25 | EventRegisterDataListener EventType = "REGISTER_DATA_LISTENER" 26 | EventUnregisterDataListener EventType = "UNREGISTER_DATA_LISTENER" 27 | EventDataUpdate EventType = "DATA_UPDATE" 28 | EventUpdateSettings EventType = "UPDATE_SETTINGS" 29 | ) 30 | -------------------------------------------------------------------------------- /event/handler/exit-application.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/timmo001/system-bridge/event" 8 | ) 9 | 10 | func RegisterExitApplicationHandler(router *event.MessageRouter) { 11 | router.RegisterSimpleHandler(event.EventExitApplication, func(connection string, message event.Message) event.MessageResponse { 12 | log.Infof("Received exit event: %v", message) 13 | 14 | log.Info("Exiting backend...") 15 | defer os.Exit(0) 16 | 17 | return event.MessageResponse{ 18 | ID: message.ID, 19 | Type: event.ResponseTypeApplicationExiting, 20 | Subtype: event.ResponseSubtypeNone, 21 | Data: message.Data, 22 | Message: "Application is exiting", 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /event/handler/file_info.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "mime" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // GetFileInfo returns detailed information about a file 10 | func GetFileInfo(basePath string, fileName string) *GetFileResponseData { 11 | fullPath := filepath.Join(basePath, fileName) 12 | info, err := os.Stat(fullPath) 13 | if err != nil { 14 | return nil 15 | } 16 | 17 | // Get file permissions as string 18 | perms := info.Mode().String() 19 | 20 | // Get content type based on extension 21 | contentType := "" 22 | extension := filepath.Ext(fileName) 23 | if !info.IsDir() && extension != "" { 24 | contentType = mime.TypeByExtension(extension) 25 | } 26 | 27 | return &GetFileResponseData{ 28 | Name: fileName, 29 | Path: fullPath, 30 | Size: info.Size(), 31 | IsDirectory: info.IsDir(), 32 | ModTime: info.ModTime(), 33 | Permissions: perms, 34 | ContentType: contentType, 35 | Extension: extension, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /event/handler/get-data.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/mitchellh/mapstructure" 6 | "github.com/timmo001/system-bridge/bus" 7 | "github.com/timmo001/system-bridge/event" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | type GetDataRequestData struct { 12 | Modules []types.ModuleName `json:"modules" mapstructure:"modules"` 13 | } 14 | 15 | type GetDataResponseData = any 16 | 17 | func RegisterGetDataHandler(router *event.MessageRouter) { 18 | router.RegisterSimpleHandler(event.EventGetData, func(connection string, message event.Message) event.MessageResponse { 19 | log.Infof("Received get data event: %v", message) 20 | 21 | var data GetDataRequestData 22 | if err := mapstructure.Decode(message.Data, &data); err != nil { 23 | return event.MessageResponse{ 24 | ID: message.ID, 25 | Type: event.ResponseTypeError, 26 | Subtype: event.ResponseSubtypeBadRequest, 27 | Message: "Invalid request data format: " + err.Error(), 28 | } 29 | } 30 | 31 | go func() { 32 | bus.GetInstance().Publish(bus.Event{ 33 | Type: bus.EventGetDataModule, 34 | Data: bus.GetDataRequest{ 35 | Connection: connection, 36 | Modules: data.Modules, 37 | }, 38 | }) 39 | }() 40 | 41 | return event.MessageResponse{ 42 | ID: message.ID, 43 | Type: event.ResponseTypeDataGet, 44 | Subtype: event.ResponseSubtypeNone, 45 | Data: data, 46 | Message: "Getting data", 47 | } 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /event/handler/get-directories.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/timmo001/system-bridge/event" 6 | "github.com/timmo001/system-bridge/utils/handlers/filesystem" 7 | ) 8 | 9 | func GetDirectories(router *event.MessageRouter) []filesystem.DirectoryInfo { 10 | directories := filesystem.GetUserDirectories() 11 | 12 | // Get user media directories 13 | for _, directory := range router.Settings.Media.Directories { 14 | directories = append(directories, filesystem.DirectoryInfo{ 15 | Key: directory.Name, 16 | Path: directory.Path, 17 | }) 18 | } 19 | 20 | return directories 21 | } 22 | 23 | func RegisterGetDirectoriesHandler(router *event.MessageRouter) { 24 | router.RegisterSimpleHandler(event.EventGetDirectories, func(connection string, message event.Message) event.MessageResponse { 25 | log.Infof("Received get directories event: %v", message) 26 | 27 | directories := GetDirectories(router) 28 | 29 | return event.MessageResponse{ 30 | ID: message.ID, 31 | Type: event.ResponseTypeDirectories, 32 | Subtype: event.ResponseSubtypeNone, 33 | Data: directories, 34 | Message: "Got directories", 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /event/handler/get-directory.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/mitchellh/mapstructure" 6 | "github.com/timmo001/system-bridge/event" 7 | ) 8 | 9 | type GetDirectoryRequestData struct { 10 | BaseDirectory string `json:"base" mapstructure:"base"` 11 | } 12 | 13 | func GetDirectory(router *event.MessageRouter, baseDirectoryKey string) *GetDirectoriesResponseDataItem { 14 | // Get directories 15 | directories := GetDirectories(router) 16 | 17 | // Find the directory with the key of the base directory 18 | for _, directory := range directories { 19 | if directory.Key == baseDirectoryKey { 20 | return &GetDirectoriesResponseDataItem{ 21 | Key: directory.Key, 22 | Name: directory.Key, // Using Key as Name since DirectoryInfo doesn't have a Name field 23 | Path: directory.Path, 24 | } 25 | } 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func RegisterGetDirectoryHandler(router *event.MessageRouter) { 32 | router.RegisterSimpleHandler(event.EventGetDirectory, func(connection string, message event.Message) event.MessageResponse { 33 | log.Infof("Received get directory event: %v", message) 34 | 35 | var data GetDirectoryRequestData 36 | if err := mapstructure.Decode(message.Data, &data); err != nil { 37 | return event.MessageResponse{ 38 | ID: message.ID, 39 | Type: event.ResponseTypeError, 40 | Subtype: event.ResponseSubtypeBadRequest, 41 | Message: "Invalid request data format: " + err.Error(), 42 | } 43 | } 44 | 45 | directory := GetDirectory(router, data.BaseDirectory) 46 | 47 | if directory == nil { 48 | return event.MessageResponse{ 49 | ID: message.ID, 50 | Type: event.ResponseTypeError, 51 | Subtype: event.ResponseSubtypeBadDirectory, 52 | Message: "Failed to get directory", 53 | } 54 | } 55 | 56 | return event.MessageResponse{ 57 | ID: message.ID, 58 | Type: event.ResponseTypeDirectory, 59 | Subtype: event.ResponseSubtypeNone, 60 | Data: directory, 61 | } 62 | }) 63 | } 64 | 65 | -------------------------------------------------------------------------------- /event/handler/get-file.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/mitchellh/mapstructure" 6 | "github.com/timmo001/system-bridge/event" 7 | "github.com/timmo001/system-bridge/utils/handlers/filesystem" 8 | ) 9 | 10 | type GetFileRequestData struct { 11 | Path string `json:"path" mapstructure:"path"` 12 | } 13 | 14 | func RegisterGetFileHandler(router *event.MessageRouter) { 15 | router.RegisterSimpleHandler(event.EventGetFile, func(connection string, message event.Message) event.MessageResponse { 16 | log.Infof("Received get file event: %v", message) 17 | 18 | data := GetFileRequestData{} 19 | err := mapstructure.Decode(message.Data, &data) 20 | if err != nil { 21 | log.Errorf("Failed to decode get file event data: %v", err) 22 | return event.MessageResponse{ 23 | ID: message.ID, 24 | Type: event.ResponseTypeError, 25 | Subtype: event.ResponseSubtypeNone, 26 | Message: "Failed to decode get file event data", 27 | } 28 | } 29 | 30 | // Validate path data 31 | if data.Path == "" { 32 | log.Error("No path provided for get file") 33 | return event.MessageResponse{ 34 | ID: message.ID, 35 | Type: event.ResponseTypeError, 36 | Subtype: event.ResponseSubtypeBadRequest, 37 | Message: "No path provided for get file", 38 | } 39 | } 40 | 41 | fileInfo, err := filesystem.GetFileInfo(data.Path) 42 | if err != nil { 43 | log.Errorf("Failed to get file info: %v", err) 44 | return event.MessageResponse{ 45 | ID: message.ID, 46 | Type: event.ResponseTypeError, 47 | Subtype: event.ResponseSubtypeNone, 48 | Message: "Failed to get file info", 49 | } 50 | } 51 | 52 | return event.MessageResponse{ 53 | ID: message.ID, 54 | Type: event.ResponseTypeFile, 55 | Subtype: event.ResponseSubtypeNone, 56 | Data: fileInfo, 57 | Message: "Got file info", 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /event/handler/get-files.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/charmbracelet/log" 8 | "github.com/mitchellh/mapstructure" 9 | "github.com/timmo001/system-bridge/event" 10 | ) 11 | 12 | type GetFilesRequestData struct { 13 | BaseDirectory string `json:"base" mapstructure:"base"` 14 | Path string `json:"path,omitempty" mapstructure:"path,omitempty"` 15 | } 16 | 17 | type GetFilesResponseData = []GetFileResponseData 18 | 19 | func GetFiles(path string) []GetFileResponseData { 20 | files, err := os.ReadDir(path) 21 | if err != nil { 22 | log.Errorf("Failed to read directory: %v", err) 23 | return nil 24 | } 25 | 26 | responseData := []GetFileResponseData{} 27 | for _, file := range files { 28 | fileInfo := GetFileInfo(path, file.Name()) 29 | responseData = append(responseData, *fileInfo) 30 | } 31 | return responseData 32 | } 33 | 34 | func RegisterGetFilesHandler(router *event.MessageRouter) { 35 | router.RegisterSimpleHandler(event.EventGetFiles, func(connection string, message event.Message) event.MessageResponse { 36 | log.Infof("Received get files event: %v", message) 37 | 38 | var data GetFilesRequestData 39 | if err := mapstructure.Decode(message.Data, &data); err != nil { 40 | return event.MessageResponse{ 41 | ID: message.ID, 42 | Type: event.ResponseTypeError, 43 | Subtype: event.ResponseSubtypeBadRequest, 44 | Message: "Invalid request data format: " + err.Error(), 45 | } 46 | } 47 | 48 | // Get base directory 49 | baseDirectory := GetDirectory(router, data.BaseDirectory) 50 | if baseDirectory == nil { 51 | return event.MessageResponse{ 52 | ID: message.ID, 53 | Type: event.ResponseTypeError, 54 | Subtype: event.ResponseSubtypeBadDirectory, 55 | } 56 | } 57 | 58 | // Get files 59 | var files []GetFileResponseData 60 | if data.Path != "" { 61 | files = GetFiles(filepath.Join(baseDirectory.Path, data.Path)) 62 | } else { 63 | files = GetFiles(baseDirectory.Path) 64 | } 65 | 66 | return event.MessageResponse{ 67 | ID: message.ID, 68 | Type: event.ResponseTypeFiles, 69 | Subtype: event.ResponseSubtypeNone, 70 | Data: files, 71 | Message: "Got files", 72 | } 73 | }) 74 | } 75 | -------------------------------------------------------------------------------- /event/handler/get-settings.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/timmo001/system-bridge/event" 6 | "github.com/timmo001/system-bridge/utils/handlers/settings" 7 | ) 8 | 9 | type GetSettingsResponseData = settings.Settings 10 | 11 | func RegisterGetSettingsHandler(router *event.MessageRouter) { 12 | router.RegisterSimpleHandler(event.EventGetSettings, func(connection string, message event.Message) event.MessageResponse { 13 | log.Infof("Received get settings event: %v", message) 14 | 15 | settings, err := settings.Load() 16 | if err != nil { 17 | log.Errorf("Failed to load settings: %v", err) 18 | return event.MessageResponse{ 19 | ID: message.ID, 20 | Type: event.ResponseTypeError, 21 | Subtype: event.ResponseSubtypeNone, 22 | Message: "Failed to load settings", 23 | } 24 | } 25 | 26 | return event.MessageResponse{ 27 | ID: message.ID, 28 | Type: event.ResponseTypeSettingsResult, 29 | Subtype: event.ResponseSubtypeNone, 30 | Data: settings, 31 | Message: "Got settings", 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /event/handler/handler.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/timmo001/system-bridge/event" 5 | ) 6 | 7 | func RegisterHandlers(router *event.MessageRouter) { 8 | RegisterExitApplicationHandler(router) 9 | RegisterGetDataHandler(router) 10 | RegisterGetDirectoriesHandler(router) 11 | RegisterGetFilesHandler(router) 12 | RegisterGetFileHandler(router) 13 | RegisterGetDirectoryHandler(router) 14 | RegisterGetSettingsHandler(router) 15 | RegisterKeyboardKeypressHandler(router) 16 | RegisterKeyboardTextHandler(router) 17 | RegisterMediaControlHandler(router) 18 | RegisterNotificationHandler(router) 19 | RegisterOpenHandler(router) 20 | RegisterPowerHibernateHandler(router) 21 | RegisterPowerLockHandler(router) 22 | RegisterPowerLogoutHandler(router) 23 | RegisterPowerRestartHandler(router) 24 | RegisterPowerShutdownHandler(router) 25 | RegisterPowerSleepHandler(router) 26 | RegisterRegisterDataListenerHandler(router) 27 | RegisterUnregisterDataListenerHandler(router) 28 | RegisterUpdateSettingsHandler(router) 29 | } 30 | -------------------------------------------------------------------------------- /event/handler/keyboard-keypress.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/mitchellh/mapstructure" 6 | "github.com/timmo001/system-bridge/event" 7 | "github.com/timmo001/system-bridge/utils/handlers/keyboard" 8 | ) 9 | 10 | func RegisterKeyboardKeypressHandler(router *event.MessageRouter) { 11 | router.RegisterSimpleHandler(event.EventKeyboardKeypress, func(connection string, message event.Message) event.MessageResponse { 12 | log.Infof("Received keyboard keypress event: %v", message) 13 | 14 | data := keyboard.KeypressData{} 15 | err := mapstructure.Decode(message.Data, &data) 16 | if err != nil { 17 | log.Errorf("Failed to decode keyboard keypress event data: %v", err) 18 | return event.MessageResponse{ 19 | ID: message.ID, 20 | Type: event.ResponseTypeError, 21 | Subtype: event.ResponseSubtypeNone, 22 | Message: "Failed to decode keyboard keypress event data", 23 | } 24 | } 25 | 26 | // Validate key data 27 | if data.Key == "" { 28 | log.Error("No key provided for keyboard keypress") 29 | return event.MessageResponse{ 30 | ID: message.ID, 31 | Type: event.ResponseTypeError, 32 | Subtype: event.ResponseSubtypeBadRequest, 33 | Message: "No key provided for keyboard keypress", 34 | } 35 | } 36 | 37 | log.Infof("Pressing keyboard key: %s with modifiers: %v", data.Key, data.Modifiers) 38 | 39 | err = keyboard.SendKeypress(data) 40 | if err != nil { 41 | log.Errorf("Failed to press key: %v", err) 42 | return event.MessageResponse{ 43 | ID: message.ID, 44 | Type: event.ResponseTypeError, 45 | Subtype: event.ResponseSubtypeNone, 46 | Message: "Failed to press key", 47 | } 48 | } 49 | 50 | log.Debugf("Key pressed: %s with modifiers: %v", data.Key, data.Modifiers) 51 | 52 | return event.MessageResponse{ 53 | ID: message.ID, 54 | Type: event.ResponseTypeKeyboardKeyPressed, 55 | Subtype: event.ResponseSubtypeNone, 56 | Data: message.Data, 57 | Message: "Keyboard key pressed", 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /event/handler/keyboard-text.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/mitchellh/mapstructure" 8 | "github.com/timmo001/system-bridge/event" 9 | "github.com/timmo001/system-bridge/utils/handlers/keyboard" 10 | ) 11 | 12 | type KeyboardTextRequestData struct { 13 | Text string `json:"text" mapstructure:"text"` 14 | Delay int `json:"delay" mapstructure:"delay"` 15 | } 16 | 17 | func RegisterKeyboardTextHandler(router *event.MessageRouter) { 18 | router.RegisterSimpleHandler(event.EventKeyboardText, func(connection string, message event.Message) event.MessageResponse { 19 | log.Infof("Received keyboard text event: %v", message) 20 | 21 | data := KeyboardTextRequestData{} 22 | err := mapstructure.Decode(message.Data, &data) 23 | if err != nil { 24 | log.Errorf("Failed to decode keyboard text event data: %v", err) 25 | return event.MessageResponse{ 26 | ID: message.ID, 27 | Type: event.ResponseTypeError, 28 | Subtype: event.ResponseSubtypeNone, 29 | Message: "Failed to decode keyboard text event data", 30 | } 31 | } 32 | 33 | // Validate text data 34 | if data.Text == "" { 35 | log.Error("No text provided for keyboard text event") 36 | return event.MessageResponse{ 37 | ID: message.ID, 38 | Type: event.ResponseTypeError, 39 | Subtype: event.ResponseSubtypeBadRequest, 40 | Message: "No text provided for keyboard text event", 41 | } 42 | } 43 | 44 | // Use provided delay 45 | if data.Delay > 0 { 46 | delay := data.Delay 47 | 48 | log.Infof("Waiting for %d milliseconds", delay) 49 | time.Sleep(time.Duration(delay) * time.Millisecond) 50 | } 51 | 52 | log.Infof("Typing text: %s", data.Text) 53 | // Type the text 54 | err = keyboard.SendText(data.Text) 55 | if err != nil { 56 | log.Errorf("Failed to type text: %v", err) 57 | return event.MessageResponse{ 58 | ID: message.ID, 59 | Type: event.ResponseTypeError, 60 | Subtype: event.ResponseSubtypeNone, 61 | Message: "Failed to type text", 62 | } 63 | } 64 | 65 | return event.MessageResponse{ 66 | ID: message.ID, 67 | Type: event.ResponseTypeKeyboardTextSent, 68 | Subtype: event.ResponseSubtypeNone, 69 | Data: message.Data, 70 | Message: "Keyboard text sent", 71 | } 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /event/handler/media-control.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/mitchellh/mapstructure" 6 | "github.com/timmo001/system-bridge/event" 7 | "github.com/timmo001/system-bridge/utils/handlers/media" 8 | ) 9 | 10 | type MediaControlRequestData struct { 11 | Action string `json:"action" mapstructure:"action"` 12 | } 13 | 14 | func RegisterMediaControlHandler(router *event.MessageRouter) { 15 | router.RegisterSimpleHandler(event.EventMediaControl, func(connection string, message event.Message) event.MessageResponse { 16 | log.Infof("Received media control event: %v", message) 17 | 18 | data := MediaControlRequestData{} 19 | err := mapstructure.Decode(message.Data, &data) 20 | if err != nil { 21 | log.Errorf("Failed to decode media control event data: %v", err) 22 | return event.MessageResponse{ 23 | ID: message.ID, 24 | Type: event.ResponseTypeError, 25 | Subtype: event.ResponseSubtypeNone, 26 | Message: "Failed to decode media control event data", 27 | } 28 | } 29 | 30 | // Validate action data 31 | if data.Action == "" { 32 | log.Error("No action provided for media control") 33 | return event.MessageResponse{ 34 | ID: message.ID, 35 | Type: event.ResponseTypeError, 36 | Subtype: event.ResponseSubtypeBadRequest, 37 | Message: "No action provided for media control", 38 | } 39 | } 40 | 41 | err = media.Control(media.MediaAction(data.Action)) 42 | if err != nil { 43 | log.Errorf("Failed to control media: %v", err) 44 | return event.MessageResponse{ 45 | ID: message.ID, 46 | Type: event.ResponseTypeError, 47 | Subtype: event.ResponseSubtypeNone, 48 | Message: "Failed to control media", 49 | } 50 | } 51 | 52 | return event.MessageResponse{ 53 | ID: message.ID, 54 | Type: event.ResponseTypeMediaControlled, 55 | Subtype: event.ResponseSubtypeNone, 56 | Data: message.Data, 57 | Message: "Media controlled", 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /event/handler/notification.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/mitchellh/mapstructure" 6 | "github.com/timmo001/system-bridge/event" 7 | "github.com/timmo001/system-bridge/utils/handlers/notification" 8 | ) 9 | 10 | func RegisterNotificationHandler(router *event.MessageRouter) { 11 | router.RegisterSimpleHandler(event.EventNotification, func(connection string, message event.Message) event.MessageResponse { 12 | log.Infof("Received notification event: %v", message) 13 | 14 | data := notification.NotificationData{} 15 | err := mapstructure.Decode(message.Data, &data) 16 | if err != nil { 17 | log.Errorf("Failed to decode notification event data: %v", err) 18 | return event.MessageResponse{ 19 | ID: message.ID, 20 | Type: event.ResponseTypeError, 21 | Subtype: event.ResponseSubtypeNone, 22 | Message: "Failed to decode notification event data", 23 | } 24 | } 25 | 26 | // Validate notification data 27 | if data.Title == "" || data.Message == "" { 28 | log.Error("Missing required notification data") 29 | return event.MessageResponse{ 30 | ID: message.ID, 31 | Type: event.ResponseTypeError, 32 | Subtype: event.ResponseSubtypeBadRequest, 33 | Message: "Missing required notification data", 34 | } 35 | } 36 | 37 | err = notification.Send(data) 38 | if err != nil { 39 | log.Errorf("Failed to send notification: %v", err) 40 | return event.MessageResponse{ 41 | ID: message.ID, 42 | Type: event.ResponseTypeError, 43 | Subtype: event.ResponseSubtypeNone, 44 | Message: "Failed to send notification", 45 | } 46 | } 47 | 48 | return event.MessageResponse{ 49 | ID: message.ID, 50 | Type: event.ResponseTypeNotificationSent, 51 | Subtype: event.ResponseSubtypeNone, 52 | Data: message.Data, 53 | Message: "Notification sent", 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /event/handler/open.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/mitchellh/mapstructure" 6 | "github.com/pkg/browser" 7 | "github.com/timmo001/system-bridge/event" 8 | "github.com/timmo001/system-bridge/utils/handlers/filesystem" 9 | ) 10 | 11 | type OpenRequestData struct { 12 | Path string `json:"path" mapstructure:"path"` 13 | URL string `json:"url" mapstructure:"url"` 14 | } 15 | 16 | func RegisterOpenHandler(router *event.MessageRouter) { 17 | router.RegisterSimpleHandler(event.EventOpen, func(connection string, message event.Message) event.MessageResponse { 18 | log.Infof("Received open event: %v", message) 19 | 20 | data := OpenRequestData{} 21 | err := mapstructure.Decode(message.Data, &data) 22 | if err != nil { 23 | log.Errorf("Failed to decode open event data: %v", err) 24 | return event.MessageResponse{ 25 | ID: message.ID, 26 | Type: event.ResponseTypeError, 27 | Subtype: event.ResponseSubtypeNone, 28 | Message: "Failed to decode open event data", 29 | } 30 | } 31 | 32 | // Validate path data 33 | if data.Path == "" && data.URL == "" { 34 | log.Error("No path or URL provided for open") 35 | return event.MessageResponse{ 36 | ID: message.ID, 37 | Type: event.ResponseTypeError, 38 | Subtype: event.ResponseSubtypeBadRequest, 39 | Message: "No path or URL provided for open", 40 | } 41 | } 42 | 43 | if data.Path != "" { 44 | err = filesystem.OpenFile(data.Path) 45 | if err != nil { 46 | log.Errorf("Failed to open file: %v", err) 47 | return event.MessageResponse{ 48 | ID: message.ID, 49 | Type: event.ResponseTypeError, 50 | Subtype: event.ResponseSubtypeNone, 51 | Message: "Failed to open file", 52 | } 53 | } 54 | 55 | return event.MessageResponse{ 56 | ID: message.ID, 57 | Type: event.ResponseTypeOpened, 58 | Subtype: event.ResponseSubtypeNone, 59 | Data: message.Data, 60 | Message: "Opened file", 61 | } 62 | } else if data.URL != "" { 63 | // Open the URL in the default browser 64 | err := browser.OpenURL(data.URL) 65 | if err != nil { 66 | log.Errorf("Failed to open URL: %v", err) 67 | return event.MessageResponse{ 68 | ID: message.ID, 69 | Type: event.ResponseTypeError, 70 | Subtype: event.ResponseSubtypeNone, 71 | Message: "Failed to open URL", 72 | } 73 | } 74 | return event.MessageResponse{ 75 | ID: message.ID, 76 | Type: event.ResponseTypeOpened, 77 | Subtype: event.ResponseSubtypeNone, 78 | Data: message.Data, 79 | Message: "Opened URL", 80 | } 81 | } 82 | 83 | return event.MessageResponse{ 84 | ID: message.ID, 85 | Type: event.ResponseTypeError, 86 | Subtype: event.ResponseSubtypeBadRequest, 87 | Message: "No path or URL provided for open", 88 | } 89 | 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /event/handler/power-hibernate.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/charmbracelet/log" 10 | "github.com/timmo001/system-bridge/event" 11 | ) 12 | 13 | func Hibernate() error { 14 | switch runtime.GOOS { 15 | case "windows": 16 | cmd := exec.Command("shutdown", "/h") 17 | return cmd.Run() 18 | case "linux": 19 | // Try systemd first 20 | cmd := exec.Command("systemctl", "hibernate") 21 | if err := cmd.Run(); err != nil { 22 | // Fallback to pm-utils if systemd fails 23 | cmd = exec.Command("pm-hibernate") 24 | return cmd.Run() 25 | } 26 | return nil 27 | case "darwin": 28 | cmd := exec.Command("pmset", "sleepnow") 29 | return cmd.Run() 30 | default: 31 | return fmt.Errorf("hibernation not supported on %s", runtime.GOOS) 32 | } 33 | } 34 | 35 | func RegisterPowerHibernateHandler(router *event.MessageRouter) { 36 | router.RegisterSimpleHandler(event.EventPowerHibernate, func(connection string, message event.Message) event.MessageResponse { 37 | log.Infof("Received power hibernate event: %v", message) 38 | 39 | go func() { 40 | time.Sleep(1 * time.Second) 41 | 42 | // Hibernate the system 43 | if err := Hibernate(); err != nil { 44 | log.Errorf("Failed to hibernate system: %v", err) 45 | } 46 | }() 47 | 48 | return event.MessageResponse{ 49 | ID: message.ID, 50 | Type: event.ResponseTypePowerHibernating, 51 | Subtype: event.ResponseSubtypeNone, 52 | Data: message.Data, 53 | Message: "Hibernating", 54 | } 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /event/handler/power-lock.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/charmbracelet/log" 10 | "github.com/timmo001/system-bridge/event" 11 | ) 12 | 13 | func Lock() error { 14 | switch runtime.GOOS { 15 | case "windows": 16 | cmd := exec.Command("rundll32.exe", "user32.dll,LockWorkStation") 17 | return cmd.Run() 18 | case "linux": 19 | // Try Wayland first with loginctl 20 | cmd := exec.Command("loginctl", "lock-session") 21 | if err := cmd.Run(); err == nil { 22 | return nil 23 | } 24 | 25 | // If Wayland fails, try X11 26 | cmd = exec.Command("xscreensaver-command", "-lock") 27 | if err := cmd.Run(); err == nil { 28 | return nil 29 | } 30 | 31 | // If xscreensaver fails, try xlock as last resort 32 | cmd = exec.Command("xlock") 33 | return cmd.Run() 34 | case "darwin": 35 | cmd := exec.Command("pmset", "displaysleepnow") 36 | return cmd.Run() 37 | default: 38 | return fmt.Errorf("locking not supported on %s", runtime.GOOS) 39 | } 40 | } 41 | 42 | func RegisterPowerLockHandler(router *event.MessageRouter) { 43 | router.RegisterSimpleHandler(event.EventPowerLock, func(connection string, message event.Message) event.MessageResponse { 44 | log.Infof("Received power lock event: %v", message) 45 | 46 | go func() { 47 | time.Sleep(1 * time.Second) 48 | 49 | // Lock the system 50 | if err := Lock(); err != nil { 51 | log.Errorf("Failed to lock system: %v", err) 52 | } 53 | }() 54 | 55 | return event.MessageResponse{ 56 | ID: message.ID, 57 | Type: event.ResponseTypePowerLocking, 58 | Subtype: event.ResponseSubtypeNone, 59 | Data: message.Data, 60 | Message: "Locking", 61 | } 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /event/handler/power-logout.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/charmbracelet/log" 10 | "github.com/timmo001/system-bridge/event" 11 | ) 12 | 13 | func Logout() error { 14 | switch runtime.GOOS { 15 | case "windows": 16 | cmd := exec.Command("shutdown", "/l") 17 | return cmd.Run() 18 | case "linux": 19 | cmd := exec.Command("loginctl", "terminate-user", "current") 20 | return cmd.Run() 21 | case "darwin": 22 | cmd := exec.Command("osascript", "-e", "tell application \"System Events\" to log out") 23 | return cmd.Run() 24 | default: 25 | return fmt.Errorf("logging out not supported on %s", runtime.GOOS) 26 | } 27 | } 28 | 29 | func RegisterPowerLogoutHandler(router *event.MessageRouter) { 30 | router.RegisterSimpleHandler(event.EventPowerLogout, func(connection string, message event.Message) event.MessageResponse { 31 | log.Infof("Received power logout event: %v", message) 32 | 33 | go func() { 34 | time.Sleep(1 * time.Second) 35 | 36 | // Logout the system 37 | if err := Logout(); err != nil { 38 | log.Errorf("Failed to logout system: %v", err) 39 | } 40 | }() 41 | 42 | return event.MessageResponse{ 43 | ID: message.ID, 44 | Type: event.ResponseTypePowerLoggingout, 45 | Subtype: event.ResponseSubtypeNone, 46 | Data: message.Data, 47 | Message: "Logging out", 48 | } 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /event/handler/power-restart.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/charmbracelet/log" 10 | "github.com/timmo001/system-bridge/event" 11 | ) 12 | 13 | func Restart() error { 14 | switch runtime.GOOS { 15 | case "windows": 16 | cmd := exec.Command("shutdown", "/r") 17 | return cmd.Run() 18 | case "linux": 19 | cmd := exec.Command("systemctl", "reboot") 20 | return cmd.Run() 21 | case "darwin": 22 | cmd := exec.Command("osascript", "-e", "tell application \"System Events\" to restart") 23 | return cmd.Run() 24 | default: 25 | return fmt.Errorf("restarting not supported on %s", runtime.GOOS) 26 | } 27 | } 28 | 29 | func RegisterPowerRestartHandler(router *event.MessageRouter) { 30 | router.RegisterSimpleHandler(event.EventPowerRestart, func(connection string, message event.Message) event.MessageResponse { 31 | log.Infof("Received power restart event: %v", message) 32 | 33 | go func() { 34 | time.Sleep(1 * time.Second) 35 | 36 | // Restart the system 37 | if err := Restart(); err != nil { 38 | log.Errorf("Failed to restart system: %v", err) 39 | } 40 | }() 41 | 42 | return event.MessageResponse{ 43 | ID: message.ID, 44 | Type: event.ResponseTypePowerRestarting, 45 | Subtype: event.ResponseSubtypeNone, 46 | Data: message.Data, 47 | Message: "Restarting", 48 | } 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /event/handler/power-shutdown.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/timmo001/system-bridge/event" 8 | "github.com/timmo001/system-bridge/utils/handlers/power" 9 | ) 10 | 11 | func RegisterPowerShutdownHandler(router *event.MessageRouter) { 12 | router.RegisterSimpleHandler(event.EventPowerShutdown, func(connection string, message event.Message) event.MessageResponse { 13 | log.Infof("Received power shutdown event: %v", message) 14 | 15 | go func() { 16 | time.Sleep(1 * time.Second) 17 | 18 | // Shutdown the system 19 | if err := power.Shutdown(); err != nil { 20 | log.Errorf("Failed to shutdown system: %v", err) 21 | } 22 | }() 23 | 24 | return event.MessageResponse{ 25 | ID: message.ID, 26 | Type: event.ResponseTypePowerShuttingdown, 27 | Subtype: event.ResponseSubtypeNone, 28 | Data: message.Data, 29 | Message: "Shutting down", 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /event/handler/power-sleep.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/charmbracelet/log" 10 | "github.com/timmo001/system-bridge/event" 11 | ) 12 | 13 | func Sleep() error { 14 | switch runtime.GOOS { 15 | case "windows": 16 | cmd := exec.Command("rundll32.exe", "powrprof.dll,SetSuspendState") 17 | return cmd.Run() 18 | case "linux": 19 | cmd := exec.Command("systemctl", "suspend") 20 | return cmd.Run() 21 | case "darwin": 22 | cmd := exec.Command("pmset", "sleepnow") 23 | return cmd.Run() 24 | default: 25 | return fmt.Errorf("sleeping not supported on %s", runtime.GOOS) 26 | } 27 | } 28 | 29 | func RegisterPowerSleepHandler(router *event.MessageRouter) { 30 | router.RegisterSimpleHandler(event.EventPowerSleep, func(connection string, message event.Message) event.MessageResponse { 31 | log.Infof("Received power sleep event: %v", message) 32 | 33 | go func() { 34 | time.Sleep(1 * time.Second) 35 | 36 | // Sleep the system 37 | if err := Sleep(); err != nil { 38 | log.Errorf("Failed to sleep system: %v", err) 39 | } 40 | }() 41 | 42 | return event.MessageResponse{ 43 | ID: message.ID, 44 | Type: event.ResponseTypePowerSleeping, 45 | Subtype: event.ResponseSubtypeNone, 46 | Data: message.Data, 47 | Message: "Sleeping", 48 | } 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /event/handler/register-data-listener.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/mitchellh/mapstructure" 6 | "github.com/timmo001/system-bridge/backend/websocket" 7 | "github.com/timmo001/system-bridge/event" 8 | "github.com/timmo001/system-bridge/types" 9 | ) 10 | 11 | type RegisterDataListenerRequestData struct { 12 | Modules []types.ModuleName `json:"modules" mapstructure:"modules"` 13 | } 14 | 15 | func RegisterRegisterDataListenerHandler(router *event.MessageRouter) { 16 | router.RegisterSimpleHandler(event.EventRegisterDataListener, func(connection string, message event.Message) event.MessageResponse { 17 | log.Infof("Received register data listener event: %v", message) 18 | 19 | var data RegisterDataListenerRequestData 20 | if err := mapstructure.Decode(message.Data, &data); err != nil { 21 | return event.MessageResponse{ 22 | ID: message.ID, 23 | Type: event.ResponseTypeError, 24 | Subtype: event.ResponseSubtypeBadRequest, 25 | Message: "Invalid request data format: " + err.Error(), 26 | } 27 | } 28 | 29 | ws := websocket.GetInstance() 30 | if ws == nil { 31 | log.Error("No websocket instance found") 32 | return event.MessageResponse{ 33 | ID: message.ID, 34 | Type: event.ResponseTypeError, 35 | Subtype: event.ResponseSubtypeNone, 36 | Message: "No websocket instance found", 37 | } 38 | } 39 | 40 | ws.RegisterDataListener(connection, data.Modules) 41 | 42 | return event.MessageResponse{ 43 | ID: message.ID, 44 | Type: event.ResponseTypeDataListenerRegistered, 45 | Subtype: event.ResponseSubtypeNone, 46 | Data: data, 47 | Message: "Listener registered", 48 | } 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /event/handler/system-bridge.autostart.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=System Bridge 3 | Comment=System Bridge 4 | Exec=system-bridge backend 5 | Icon=system-bridge 6 | Type=Application 7 | Categories=Utility; 8 | X-GNOME-Autostart-enabled=true 9 | -------------------------------------------------------------------------------- /event/handler/types.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import "time" 4 | 5 | // GetFileResponseData represents information about a file 6 | type GetFileResponseData struct { 7 | Name string `json:"name" mapstructure:"name"` 8 | Path string `json:"path" mapstructure:"path"` 9 | Size int64 `json:"size" mapstructure:"size"` 10 | IsDirectory bool `json:"isDirectory" mapstructure:"isDirectory"` 11 | ModTime time.Time `json:"modTime" mapstructure:"modTime"` 12 | Permissions string `json:"permissions" mapstructure:"permissions"` 13 | ContentType string `json:"contentType,omitempty" mapstructure:"contentType,omitempty"` 14 | Extension string `json:"extension,omitempty" mapstructure:"extension,omitempty"` 15 | } 16 | 17 | // GetDirectoriesResponseDataItem represents a directory item in the response 18 | type GetDirectoriesResponseDataItem struct { 19 | Key string `json:"key" mapstructure:"key"` 20 | Name string `json:"name" mapstructure:"name"` 21 | Path string `json:"path" mapstructure:"path"` 22 | Description string `json:"description,omitempty" mapstructure:"description,omitempty"` 23 | } 24 | -------------------------------------------------------------------------------- /event/handler/unregister-data-listener.go: -------------------------------------------------------------------------------- 1 | package event_handler 2 | 3 | import ( 4 | "github.com/charmbracelet/log" 5 | "github.com/timmo001/system-bridge/backend/websocket" 6 | "github.com/timmo001/system-bridge/event" 7 | ) 8 | 9 | func RegisterUnregisterDataListenerHandler(router *event.MessageRouter) { 10 | router.RegisterSimpleHandler(event.EventUnregisterDataListener, func(connection string, message event.Message) event.MessageResponse { 11 | log.Infof("Received unregister data listener event: %v", message) 12 | 13 | ws := websocket.GetInstance() 14 | if ws == nil { 15 | log.Error("No websocket instance found") 16 | return event.MessageResponse{ 17 | ID: message.ID, 18 | Type: event.ResponseTypeError, 19 | Subtype: event.ResponseSubtypeNone, 20 | Message: "No websocket instance found", 21 | } 22 | } 23 | 24 | ws.UnregisterDataListener(connection) 25 | 26 | return event.MessageResponse{ 27 | ID: message.ID, 28 | Type: event.ResponseTypeDataListenerUnregistered, 29 | } 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /lib/sensors/windows/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all files in the bin directory 2 | bin/ 3 | -------------------------------------------------------------------------------- /settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/charmbracelet/log" 7 | "github.com/spf13/viper" 8 | "github.com/timmo001/system-bridge/utils" 9 | ) 10 | 11 | type SettingsAPI struct { 12 | Token string `json:"token" mapstructure:"token"` 13 | Port int `json:"port" mapstructure:"port"` 14 | } 15 | 16 | type SettingsHotkey struct { 17 | Name string `json:"name" mapstructure:"name"` 18 | Key string `json:"key" mapstructure:"key"` 19 | } 20 | 21 | type SettingsMediaDirectory struct { 22 | Name string `json:"name" mapstructure:"name"` 23 | Path string `json:"path" mapstructure:"path"` 24 | } 25 | 26 | type SettingsMedia struct { 27 | Directories []SettingsMediaDirectory `json:"directories" mapstructure:"directories"` 28 | } 29 | 30 | type Settings struct { 31 | API SettingsAPI `json:"api" mapstructure:"api"` 32 | Autostart bool `json:"autostart" mapstructure:"autostart"` 33 | Hotkeys []SettingsHotkey `json:"hotkeys" mapstructure:"hotkeys"` 34 | LogLevel log.Level `json:"log_level" mapstructure:"log_level"` 35 | Media SettingsMedia `json:"media" mapstructure:"media"` 36 | } 37 | 38 | func Load() (*Settings, error) { 39 | viper.AutomaticEnv() 40 | 41 | viper.SetConfigName("settings") 42 | viper.SetConfigType("json") 43 | 44 | configDirPath, err := utils.GetConfigPath() 45 | if err != nil { 46 | return nil, fmt.Errorf("could not get config path: %w", err) 47 | } 48 | viper.AddConfigPath(configDirPath) 49 | 50 | // Set default values 51 | viper.SetDefault("api.token", utils.GenerateToken()) 52 | viper.SetDefault("api.port", 9170) 53 | viper.SetDefault("autostart", false) 54 | viper.SetDefault("hotkeys", []SettingsHotkey{}) 55 | viper.SetDefault("log_level", log.InfoLevel) 56 | viper.SetDefault("media.directories", []SettingsMediaDirectory{}) 57 | 58 | // Read the config file 59 | if err := viper.ReadInConfig(); err != nil { 60 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 61 | if err := viper.SafeWriteConfig(); err != nil { 62 | return nil, fmt.Errorf("error writing default config file: %w", err) 63 | } 64 | } else { 65 | return nil, fmt.Errorf("error reading config file: %w", err) 66 | } 67 | } 68 | 69 | var cfg Settings 70 | if err := viper.Unmarshal(&cfg); err != nil { 71 | return nil, fmt.Errorf("unable to decode into struct: %w", err) 72 | } 73 | 74 | return &cfg, nil 75 | } 76 | 77 | func (cfg *Settings) Save() error { 78 | viper.Set("api.token", cfg.API.Token) 79 | viper.Set("api.port", cfg.API.Port) 80 | viper.Set("autostart", cfg.Autostart) 81 | viper.Set("hotkeys", cfg.Hotkeys) 82 | viper.Set("log_level", cfg.LogLevel) 83 | viper.Set("media.directories", cfg.Media.Directories) 84 | 85 | if err := viper.WriteConfig(); err != nil { 86 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 87 | if err := viper.SafeWriteConfig(); err != nil { 88 | return fmt.Errorf("error writing config file: %w", err) 89 | } 90 | } else { 91 | return fmt.Errorf("error writing config file: %w", err) 92 | } 93 | } 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /types/battery.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // BatteryData represents battery information 4 | type BatteryData struct { 5 | IsCharging *bool `json:"is_charging"` 6 | Percentage *float64 `json:"percentage"` 7 | TimeRemaining *float64 `json:"time_remaining"` // Seconds remaining 8 | } 9 | -------------------------------------------------------------------------------- /types/cpu.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // CPUFrequency represents CPU frequency information 4 | type CPUFrequency struct { 5 | Current *float64 `json:"current"` 6 | Min *float64 `json:"min"` // TODO: Implement minimum frequency detection 7 | Max *float64 `json:"max"` // TODO: Implement maximum frequency detection 8 | } 9 | 10 | // CPUStats represents CPU statistics 11 | type CPUStats struct { 12 | // TODO: Implement CPU statistics collection 13 | CtxSwitches *int64 `json:"ctx_switches"` // Context switches count 14 | Interrupts *int64 `json:"interrupts"` // Hardware interrupts count 15 | SoftInterrupts *int64 `json:"soft_interrupts"` // Software interrupts count 16 | Syscalls *int64 `json:"syscalls"` // System calls count 17 | } 18 | 19 | // CPUTimes represents CPU timing information 20 | type CPUTimes struct { 21 | User *float64 `json:"user"` 22 | System *float64 `json:"system"` 23 | Idle *float64 `json:"idle"` 24 | Interrupt *float64 `json:"interrupt"` 25 | DPC *float64 `json:"dpc"` // TODO: Implement Deferred Procedure Call time tracking 26 | } 27 | 28 | // PerCPU represents per-CPU information 29 | type PerCPU struct { 30 | ID int `json:"id"` 31 | Frequency *CPUFrequency `json:"frequency"` 32 | Power *float64 `json:"power"` // TODO: Implement per-CPU power consumption monitoring 33 | Times *CPUTimes `json:"times"` 34 | TimesPercent *CPUTimes `json:"times_percent"` // TODO: Implement per-CPU time percentage calculations 35 | Usage *float64 `json:"usage"` 36 | Voltage *float64 `json:"voltage"` // TODO: Implement per-CPU voltage monitoring 37 | } 38 | 39 | // CPUData represents overall CPU information 40 | type CPUData struct { 41 | Count *int `json:"count"` 42 | Frequency *CPUFrequency `json:"frequency"` 43 | LoadAverage *float64 `json:"load_average"` 44 | PerCPU []PerCPU `json:"per_cpu"` 45 | Power *float64 `json:"power"` // TODO: Implement overall CPU power consumption monitoring 46 | Stats *CPUStats `json:"stats"` // TODO: Implement overall CPU statistics collection 47 | Temperature *float64 `json:"temperature"` 48 | Times *CPUTimes `json:"times"` 49 | TimesPercent *CPUTimes `json:"times_percent"` // TODO: Implement overall CPU time percentage calculations 50 | Usage *float64 `json:"usage"` 51 | Voltage *float64 `json:"voltage"` // TODO: Implement overall CPU voltage monitoring 52 | } 53 | -------------------------------------------------------------------------------- /types/disks.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // DiskIOCounters represents disk I/O statistics 4 | type DiskIOCounters struct { 5 | ReadCount uint64 `json:"read_count"` 6 | WriteCount uint64 `json:"write_count"` 7 | ReadBytes uint64 `json:"read_bytes"` 8 | WriteBytes uint64 `json:"write_bytes"` 9 | ReadTime uint64 `json:"read_time"` 10 | WriteTime uint64 `json:"write_time"` 11 | } 12 | 13 | // DiskUsage represents disk space usage information 14 | type DiskUsage struct { 15 | Total uint64 `json:"total"` 16 | Used uint64 `json:"used"` 17 | Free uint64 `json:"free"` 18 | Percent float64 `json:"percent"` 19 | } 20 | 21 | // DiskPartition represents information about a disk partition 22 | type DiskPartition struct { 23 | Device string `json:"device"` 24 | MountPoint string `json:"mount_point"` 25 | FilesystemType string `json:"filesystem_type"` 26 | Options string `json:"options"` 27 | MaxFileSize int64 `json:"max_file_size"` 28 | MaxPathLength int64 `json:"max_path_length"` 29 | Usage *DiskUsage `json:"usage"` 30 | } 31 | 32 | // Disk represents information about a single disk device 33 | type Disk struct { 34 | Name string `json:"name"` 35 | Partitions []DiskPartition `json:"partitions"` 36 | IOCounters *DiskIOCounters `json:"io_counters"` 37 | } 38 | 39 | // DisksData represents information about all disk devices 40 | type DisksData struct { 41 | Devices []Disk `json:"devices"` 42 | IOCounters *DiskIOCounters `json:"io_counters"` 43 | } 44 | -------------------------------------------------------------------------------- /types/displays.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Display represents information about a display device 4 | type Display struct { 5 | ID string `json:"id"` 6 | Name string `json:"name"` 7 | ResolutionHorizontal int `json:"resolution_horizontal"` 8 | ResolutionVertical int `json:"resolution_vertical"` 9 | X int `json:"x"` 10 | Y int `json:"y"` 11 | Width *int `json:"width"` 12 | Height *int `json:"height"` 13 | IsPrimary *bool `json:"is_primary"` 14 | PixelClock *float64 `json:"pixel_clock"` 15 | RefreshRate *float64 `json:"refresh_rate"` 16 | } 17 | 18 | // DisplaysData represents information about all display devices 19 | type DisplaysData = []Display 20 | -------------------------------------------------------------------------------- /types/gpus.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // GPU represents information about a GPU device 4 | type GPU struct { 5 | ID string `json:"id"` 6 | Name string `json:"name"` 7 | CoreClock *float64 `json:"core_clock"` 8 | CoreLoad *float64 `json:"core_load"` 9 | FanSpeed *float64 `json:"fan_speed"` 10 | MemoryClock *float64 `json:"memory_clock"` 11 | MemoryLoad *float64 `json:"memory_load"` 12 | MemoryFree *float64 `json:"memory_free"` 13 | MemoryUsed *float64 `json:"memory_used"` 14 | MemoryTotal *float64 `json:"memory_total"` 15 | PowerUsage *float64 `json:"power_usage"` 16 | Temperature *float64 `json:"temperature"` 17 | } 18 | 19 | // GPUsData represents information about all GPU devices 20 | type GPUsData = []GPU 21 | -------------------------------------------------------------------------------- /types/media.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // MediaData represents media information 4 | type MediaData struct { 5 | AlbumArtist *string `json:"album_artist"` 6 | AlbumTitle *string `json:"album_title"` 7 | Artist *string `json:"artist"` 8 | Duration *float64 `json:"duration"` 9 | IsFastForwardEnabled *bool `json:"is_fast_forward_enabled"` 10 | IsNextEnabled *bool `json:"is_next_enabled"` 11 | IsPauseEnabled *bool `json:"is_pause_enabled"` 12 | IsPlayEnabled *bool `json:"is_play_enabled"` 13 | IsPreviousEnabled *bool `json:"is_previous_enabled"` 14 | IsRewindEnabled *bool `json:"is_rewind_enabled"` 15 | IsStopEnabled *bool `json:"is_stop_enabled"` 16 | PlaybackRate *float64 `json:"playback_rate"` 17 | Position *float64 `json:"position"` 18 | Repeat *string `json:"repeat"` 19 | Shuffle *bool `json:"shuffle"` 20 | Status *string `json:"status"` 21 | Subtitle *string `json:"subtitle"` 22 | Thumbnail *string `json:"thumbnail"` 23 | Title *string `json:"title"` 24 | TrackNumber *int `json:"track_number"` 25 | Type *string `json:"type"` 26 | UpdatedAt *float64 `json:"updated_at"` 27 | Volume *float64 `json:"volume"` 28 | } 29 | -------------------------------------------------------------------------------- /types/memory.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // MemorySwap represents swap memory information 4 | type MemorySwap struct { 5 | Total *uint64 `json:"total"` 6 | Used *uint64 `json:"used"` 7 | Free *uint64 `json:"free"` 8 | Percent *float64 `json:"percent"` 9 | Sin *uint64 `json:"sin"` 10 | Sout *uint64 `json:"sout"` 11 | } 12 | 13 | // MemoryVirtual represents virtual memory information 14 | type MemoryVirtual struct { 15 | Total *uint64 `json:"total"` 16 | Available *uint64 `json:"available"` 17 | Percent *float64 `json:"percent"` 18 | Used *uint64 `json:"used"` 19 | Free *uint64 `json:"free"` 20 | Active *uint64 `json:"active"` 21 | Inactive *uint64 `json:"inactive"` 22 | Buffers *uint64 `json:"buffers"` 23 | Cached *uint64 `json:"cached"` 24 | Wired *uint64 `json:"wired"` 25 | Shared *uint64 `json:"shared"` 26 | } 27 | 28 | // MemoryData represents overall memory information 29 | type MemoryData struct { 30 | Swap *MemorySwap `json:"swap"` 31 | Virtual *MemoryVirtual `json:"virtual"` 32 | } 33 | -------------------------------------------------------------------------------- /types/module.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "context" 4 | 5 | // ModuleName represents the type of data module 6 | type ModuleName string 7 | 8 | const ( 9 | ModuleBattery ModuleName = "battery" 10 | ModuleCPU ModuleName = "cpu" 11 | ModuleDisks ModuleName = "disks" 12 | ModuleDisplays ModuleName = "displays" 13 | ModuleGPUs ModuleName = "gpus" 14 | ModuleMedia ModuleName = "media" 15 | ModuleMemory ModuleName = "memory" 16 | ModuleNetworks ModuleName = "networks" 17 | ModuleProcesses ModuleName = "processes" 18 | ModuleSensors ModuleName = "sensors" 19 | ModuleSystem ModuleName = "system" 20 | ) 21 | 22 | type Updater interface { 23 | Name() ModuleName 24 | Update(context.Context) (any, error) 25 | } 26 | 27 | // Module represents a data module 28 | type Module struct { 29 | Name ModuleName `json:"module" mapstructure:"module"` 30 | Updater Updater 31 | Data any `json:"data" mapstructure:"data"` 32 | Updated string `json:"updated" mapstructure:"updated"` 33 | } 34 | -------------------------------------------------------------------------------- /types/networks.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // NetworkAddress represents information about a network address 4 | type NetworkAddress struct { 5 | Address *string `json:"address"` 6 | Family *string `json:"family"` 7 | Netmask *string `json:"netmask"` 8 | Broadcast *string `json:"broadcast"` 9 | PTP *string `json:"ptp"` 10 | } 11 | 12 | // NetworkStats represents network interface statistics 13 | type NetworkStats struct { 14 | IsUp *bool `json:"isup"` 15 | Duplex *string `json:"duplex"` 16 | Speed *int `json:"speed"` 17 | MTU *int `json:"mtu"` 18 | Flags []string `json:"flags"` 19 | } 20 | 21 | // NetworkConnection represents a network connection 22 | type NetworkConnection struct { 23 | FD *int `json:"fd"` 24 | Family *int `json:"family"` 25 | Type *int `json:"type"` 26 | LAddr *string `json:"laddr"` 27 | RAddr *string `json:"raddr"` 28 | Status *string `json:"status"` 29 | PID *int `json:"pid"` 30 | } 31 | 32 | // NetworkIO represents network I/O statistics 33 | type NetworkIO struct { 34 | BytesSent *int64 `json:"bytes_sent"` 35 | BytesRecv *int64 `json:"bytes_recv"` 36 | PacketsSent *int64 `json:"packets_sent"` 37 | PacketsRecv *int64 `json:"packets_recv"` 38 | ErrIn *int64 `json:"errin"` 39 | ErrOut *int64 `json:"errout"` 40 | DropIn *int64 `json:"dropin"` 41 | DropOut *int64 `json:"dropout"` 42 | } 43 | 44 | // Network represents information about a network interface 45 | type Network struct { 46 | Name *string `json:"name"` 47 | Addresses []NetworkAddress `json:"addresses"` 48 | Stats *NetworkStats `json:"stats"` 49 | } 50 | 51 | // NetworksData represents information about all network interfaces and connections 52 | type NetworksData struct { 53 | Connections []NetworkConnection `json:"connections"` 54 | IO *NetworkIO `json:"io"` 55 | Networks []Network `json:"networks"` 56 | } 57 | -------------------------------------------------------------------------------- /types/processes.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Process represents information about a system process 4 | type Process struct { 5 | ID float64 `json:"id"` 6 | Name *string `json:"name"` 7 | CPUUsage *float64 `json:"cpu_usage"` 8 | Created *float64 `json:"created"` 9 | MemoryUsage *float64 `json:"memory_usage"` 10 | Path *string `json:"path"` 11 | Status *string `json:"status"` 12 | Username *string `json:"username"` 13 | WorkingDirectory *string `json:"working_directory"` 14 | } 15 | 16 | // ProcessesData represents information about all system processes 17 | type ProcessesData = []Process 18 | -------------------------------------------------------------------------------- /types/system.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type RunMode string 4 | 5 | const ( 6 | RunModeStandalone RunMode = "standalone" 7 | ) 8 | 9 | // SystemUser represents information about a system user 10 | type SystemUser struct { 11 | Name string `json:"name" mapstructure:"name"` 12 | Active bool `json:"active" mapstructure:"active"` 13 | Terminal string `json:"terminal" mapstructure:"terminal"` 14 | Host string `json:"host" mapstructure:"host"` 15 | Started int `json:"started" mapstructure:"started"` 16 | PID float64 `json:"pid" mapstructure:"pid"` 17 | } 18 | 19 | // SystemData represents system information 20 | type SystemData struct { 21 | BootTime uint64 `json:"boot_time" mapstructure:"boot_time"` 22 | FQDN string `json:"fqdn" mapstructure:"fqdn"` 23 | Hostname string `json:"hostname" mapstructure:"hostname"` 24 | IPAddress4 string `json:"ip_address_4" mapstructure:"ip_address_4"` 25 | MACAddress string `json:"mac_address" mapstructure:"mac_address"` 26 | PlatformVersion string `json:"platform_version" mapstructure:"platform_version"` 27 | Platform string `json:"platform" mapstructure:"platform"` 28 | Uptime uint64 `json:"uptime" mapstructure:"uptime"` 29 | Users []SystemUser `json:"users" mapstructure:"users"` 30 | UUID string `json:"uuid" mapstructure:"uuid"` 31 | Version string `json:"version" mapstructure:"version"` 32 | CameraUsage []string `json:"camera_usage" mapstructure:"camera_usage"` 33 | IPAddress6 string `json:"ip_address_6" mapstructure:"ip_address_6"` 34 | PendingReboot *bool `json:"pending_reboot" mapstructure:"pending_reboot"` 35 | RunMode RunMode `json:"run_mode" mapstructure:"run_mode"` 36 | VersionLatestURL *string `json:"version_latest_url" mapstructure:"version_latest_url"` 37 | VersionLatest *string `json:"version_latest" mapstructure:"version_latest"` 38 | VersionNewerAvailable *bool `json:"version_newer_available" mapstructure:"version_newer_available"` 39 | } 40 | -------------------------------------------------------------------------------- /utils/handlers/filesystem/filesystem.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | // DirectoryInfo represents information about a directory 4 | type DirectoryInfo struct { 5 | Key string `json:"key" mapstructure:"key"` 6 | Path string `json:"path" mapstructure:"path"` 7 | } 8 | 9 | // FileInfo represents information about a file 10 | type FileInfo struct { 11 | Name string `json:"name" mapstructure:"name"` 12 | Path string `json:"path" mapstructure:"path"` 13 | Size int64 `json:"size" mapstructure:"size"` 14 | Modified int64 `json:"modified" mapstructure:"modified"` 15 | Extension string `json:"extension" mapstructure:"extension"` 16 | MimeType string `json:"mime_type" mapstructure:"mime_type"` 17 | } 18 | 19 | // GetUserDirectories returns a list of user directories 20 | func GetUserDirectories() []DirectoryInfo { 21 | return getUserDirectories() 22 | } 23 | 24 | // GetDirectoryContents returns the contents of a directory 25 | func GetDirectoryContents(path string) ([]FileInfo, error) { 26 | return getDirectoryContents(path) 27 | } 28 | 29 | // GetFileInfo returns information about a file 30 | func GetFileInfo(path string) (*FileInfo, error) { 31 | return getFileInfo(path) 32 | } 33 | 34 | // OpenFile opens a file with the default application 35 | func OpenFile(path string) error { 36 | return openFile(path) 37 | } 38 | -------------------------------------------------------------------------------- /utils/handlers/filesystem/filesystem_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package filesystem 5 | 6 | import ( 7 | "mime" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | func getUserDirectories() []DirectoryInfo { 15 | homeDir, err := os.UserHomeDir() 16 | if err != nil { 17 | return nil 18 | } 19 | 20 | return []DirectoryInfo{ 21 | {Key: "desktop", Path: filepath.Join(homeDir, "Desktop")}, 22 | {Key: "documents", Path: filepath.Join(homeDir, "Documents")}, 23 | {Key: "downloads", Path: filepath.Join(homeDir, "Downloads")}, 24 | {Key: "music", Path: filepath.Join(homeDir, "Music")}, 25 | {Key: "pictures", Path: filepath.Join(homeDir, "Pictures")}, 26 | {Key: "videos", Path: filepath.Join(homeDir, "Movies")}, 27 | } 28 | } 29 | 30 | func getDirectoryContents(path string) ([]FileInfo, error) { 31 | entries, err := os.ReadDir(path) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | var files []FileInfo 37 | for _, entry := range entries { 38 | filePath := filepath.Join(path, entry.Name()) 39 | fileInfo, err := getFileInfo(filePath) 40 | if err != nil { 41 | continue 42 | } 43 | 44 | files = append(files, *fileInfo) 45 | } 46 | 47 | return files, nil 48 | } 49 | 50 | func getFileInfo(path string) (*FileInfo, error) { 51 | info, err := os.Stat(path) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | ext := filepath.Ext(path) 57 | mimeType := mime.TypeByExtension(ext) 58 | if mimeType == "" { 59 | mimeType = "application/octet-stream" 60 | } 61 | 62 | return &FileInfo{ 63 | Name: info.Name(), 64 | Path: path, 65 | Size: info.Size(), 66 | Modified: info.ModTime().UnixMilli(), 67 | Extension: strings.TrimPrefix(ext, "."), 68 | MimeType: mimeType, 69 | }, nil 70 | } 71 | 72 | func openFile(path string) error { 73 | cmd := exec.Command("open", path) 74 | return cmd.Run() 75 | } 76 | -------------------------------------------------------------------------------- /utils/handlers/filesystem/filesystem_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package filesystem 5 | 6 | import ( 7 | "mime" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | func getUserDirectories() []DirectoryInfo { 15 | homeDir, err := os.UserHomeDir() 16 | if err != nil { 17 | return nil 18 | } 19 | 20 | configHome := os.Getenv("XDG_CONFIG_HOME") 21 | if configHome == "" { 22 | configHome = filepath.Join(homeDir, ".config") 23 | } 24 | 25 | userDirs := filepath.Join(configHome, "user-dirs.dirs") 26 | if _, err := os.Stat(userDirs); err == nil { 27 | content, err := os.ReadFile(userDirs) 28 | if err == nil { 29 | lines := strings.Split(string(content), "\n") 30 | dirs := make(map[string]string) 31 | for _, line := range lines { 32 | if strings.HasPrefix(line, "XDG_") && strings.Contains(line, "_DIR=") { 33 | parts := strings.SplitN(line, "=", 2) 34 | if len(parts) == 2 { 35 | key := strings.ToLower(strings.TrimPrefix(parts[0], "XDG_")) 36 | key = strings.TrimSuffix(key, "_DIR") 37 | path := strings.Trim(parts[1], "\"") 38 | path = strings.ReplaceAll(path, "$HOME", homeDir) 39 | dirs[key] = path 40 | } 41 | } 42 | } 43 | 44 | return []DirectoryInfo{ 45 | {Key: "desktop", Path: dirs["desktop"]}, 46 | {Key: "documents", Path: dirs["documents"]}, 47 | {Key: "downloads", Path: dirs["download"]}, 48 | {Key: "music", Path: dirs["music"]}, 49 | {Key: "pictures", Path: dirs["pictures"]}, 50 | {Key: "videos", Path: dirs["videos"]}, 51 | } 52 | } 53 | } 54 | 55 | // Fallback to default XDG directories 56 | return []DirectoryInfo{ 57 | {Key: "desktop", Path: filepath.Join(homeDir, "Desktop")}, 58 | {Key: "documents", Path: filepath.Join(homeDir, "Documents")}, 59 | {Key: "downloads", Path: filepath.Join(homeDir, "Downloads")}, 60 | {Key: "music", Path: filepath.Join(homeDir, "Music")}, 61 | {Key: "pictures", Path: filepath.Join(homeDir, "Pictures")}, 62 | {Key: "videos", Path: filepath.Join(homeDir, "Videos")}, 63 | } 64 | } 65 | 66 | func getDirectoryContents(path string) ([]FileInfo, error) { 67 | entries, err := os.ReadDir(path) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | var files []FileInfo 73 | for _, entry := range entries { 74 | _, err := entry.Info() 75 | if err != nil { 76 | continue 77 | } 78 | 79 | filePath := filepath.Join(path, entry.Name()) 80 | fileInfo, err := getFileInfo(filePath) 81 | if err != nil { 82 | continue 83 | } 84 | 85 | files = append(files, *fileInfo) 86 | } 87 | 88 | return files, nil 89 | } 90 | 91 | func getFileInfo(path string) (*FileInfo, error) { 92 | info, err := os.Stat(path) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | ext := filepath.Ext(path) 98 | mimeType := mime.TypeByExtension(ext) 99 | if mimeType == "" { 100 | mimeType = "application/octet-stream" 101 | } 102 | 103 | return &FileInfo{ 104 | Name: info.Name(), 105 | Path: path, 106 | Size: info.Size(), 107 | Modified: info.ModTime().UnixMilli(), 108 | Extension: strings.TrimPrefix(ext, "."), 109 | MimeType: mimeType, 110 | }, nil 111 | } 112 | 113 | func openFile(path string) error { 114 | cmd := exec.Command("xdg-open", path) 115 | return cmd.Run() 116 | } 117 | -------------------------------------------------------------------------------- /utils/handlers/filesystem/filesystem_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package filesystem 5 | 6 | import ( 7 | "encoding/json" 8 | "mime" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strings" 13 | "syscall" 14 | ) 15 | 16 | func getUserDirectories() []DirectoryInfo { 17 | // Get known folder paths using PowerShell 18 | script := ` 19 | $folders = @{ 20 | "Desktop" = [System.Environment]::GetFolderPath("Desktop") 21 | "Documents" = [System.Environment]::GetFolderPath("MyDocuments") 22 | "Downloads" = (New-Object -ComObject Shell.Application).NameSpace("shell:Downloads").Self.Path 23 | "Music" = [System.Environment]::GetFolderPath("MyMusic") 24 | "Pictures" = [System.Environment]::GetFolderPath("MyPictures") 25 | "Videos" = [System.Environment]::GetFolderPath("MyVideos") 26 | } 27 | $folders | ConvertTo-Json 28 | ` 29 | cmd := exec.Command("powershell", "-Command", script) 30 | output, err := cmd.Output() 31 | if err != nil { 32 | // Fallback to default paths 33 | homeDir, err := os.UserHomeDir() 34 | if err != nil { 35 | return nil 36 | } 37 | 38 | return []DirectoryInfo{ 39 | {Key: "desktop", Path: filepath.Join(homeDir, "Desktop")}, 40 | {Key: "documents", Path: filepath.Join(homeDir, "Documents")}, 41 | {Key: "downloads", Path: filepath.Join(homeDir, "Downloads")}, 42 | {Key: "music", Path: filepath.Join(homeDir, "Music")}, 43 | {Key: "pictures", Path: filepath.Join(homeDir, "Pictures")}, 44 | {Key: "videos", Path: filepath.Join(homeDir, "Videos")}, 45 | } 46 | } 47 | 48 | // Parse JSON output 49 | var dirs map[string]string 50 | if err := json.Unmarshal(output, &dirs); err != nil { 51 | return nil 52 | } 53 | 54 | return []DirectoryInfo{ 55 | {Key: "desktop", Path: dirs["Desktop"]}, 56 | {Key: "documents", Path: dirs["Documents"]}, 57 | {Key: "downloads", Path: dirs["Downloads"]}, 58 | {Key: "music", Path: dirs["Music"]}, 59 | {Key: "pictures", Path: dirs["Pictures"]}, 60 | {Key: "videos", Path: dirs["Videos"]}, 61 | } 62 | } 63 | 64 | func getDirectoryContents(path string) ([]FileInfo, error) { 65 | entries, err := os.ReadDir(path) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | var files []FileInfo 71 | for _, entry := range entries { 72 | filePath := filepath.Join(path, entry.Name()) 73 | fileInfo, err := getFileInfo(filePath) 74 | if err != nil { 75 | continue 76 | } 77 | 78 | files = append(files, *fileInfo) 79 | } 80 | 81 | return files, nil 82 | } 83 | 84 | func getFileInfo(path string) (*FileInfo, error) { 85 | info, err := os.Stat(path) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | ext := filepath.Ext(path) 91 | mimeType := mime.TypeByExtension(ext) 92 | if mimeType == "" { 93 | mimeType = "application/octet-stream" 94 | } 95 | 96 | return &FileInfo{ 97 | Name: info.Name(), 98 | Path: path, 99 | Size: info.Size(), 100 | Modified: info.ModTime().UnixMilli(), 101 | Extension: strings.TrimPrefix(ext, "."), 102 | MimeType: mimeType, 103 | }, nil 104 | } 105 | 106 | func openFile(path string) error { 107 | cmd := exec.Command("cmd", "/c", "start", "", path) 108 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} 109 | return cmd.Run() 110 | } 111 | -------------------------------------------------------------------------------- /utils/handlers/keyboard/keyboard.go: -------------------------------------------------------------------------------- 1 | package keyboard 2 | 3 | // KeypressData represents the data needed for a keyboard keypress 4 | type KeypressData struct { 5 | Key string `json:"key" mapstructure:"key"` 6 | Modifiers []string `json:"modifiers" mapstructure:"modifiers"` 7 | Delay int `json:"delay" mapstructure:"delay"` // Delay in milliseconds 8 | } 9 | 10 | // SendKeypress sends a keyboard keypress with optional modifiers and delay 11 | func SendKeypress(data KeypressData) error { 12 | return sendKeypress(data) 13 | } 14 | 15 | // SendText sends text input 16 | func SendText(text string) error { 17 | return sendText(text) 18 | } 19 | -------------------------------------------------------------------------------- /utils/handlers/keyboard/keyboard_other.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package keyboard 5 | 6 | import ( 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-vgo/robotgo" 11 | ) 12 | 13 | func sendKeypress(data KeypressData) error { 14 | // Use provided delay 15 | if data.Delay > 0 { 16 | time.Sleep(time.Duration(data.Delay) * time.Millisecond) 17 | } 18 | 19 | // Convert modifiers to robotgo format 20 | var modifiers []any 21 | for _, mod := range data.Modifiers { 22 | mod = strings.ToLower(mod) 23 | switch mod { 24 | case "shift": 25 | modifiers = append(modifiers, "shift") 26 | case "ctrl", "control": 27 | modifiers = append(modifiers, "ctrl") 28 | case "alt": 29 | modifiers = append(modifiers, "alt") 30 | case "cmd", "command": 31 | modifiers = append(modifiers, "cmd") 32 | } 33 | } 34 | 35 | if len(modifiers) > 0 { 36 | return robotgo.KeyTap(data.Key, modifiers...) 37 | } 38 | return robotgo.KeyTap(data.Key) 39 | } 40 | 41 | func sendText(text string) error { 42 | robotgo.TypeStr(text) 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /utils/handlers/keyboard/keyboard_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package keyboard 5 | 6 | import ( 7 | "errors" 8 | "time" 9 | ) 10 | 11 | func sendKeypress(data KeypressData) error { 12 | // Use provided delay 13 | if data.Delay > 0 { 14 | time.Sleep(time.Duration(data.Delay) * time.Millisecond) 15 | } 16 | 17 | // TODO: Find implementation 18 | return errors.New("keyboard automation not supported on this platform") 19 | } 20 | 21 | func sendText(text string) error { 22 | // TODO: Find implementation 23 | return errors.New("keyboard automation not supported on this platform") 24 | } 25 | -------------------------------------------------------------------------------- /utils/handlers/media/media.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | // MediaAction represents the type of media control action 4 | type MediaAction string 5 | 6 | const ( 7 | // MediaActionPlay represents the play action 8 | MediaActionPlay MediaAction = "play" 9 | // MediaActionPause represents the pause action 10 | MediaActionPause MediaAction = "pause" 11 | // MediaActionNext represents the next track action 12 | MediaActionNext MediaAction = "next" 13 | // MediaActionPrevious represents the previous track action 14 | MediaActionPrevious MediaAction = "previous" 15 | // MediaActionStop represents the stop action 16 | MediaActionStop MediaAction = "stop" 17 | // MediaActionVolumeUp represents the volume up action 18 | MediaActionVolumeUp MediaAction = "volume_up" 19 | // MediaActionVolumeDown represents the volume down action 20 | MediaActionVolumeDown MediaAction = "volume_down" 21 | // MediaActionMute represents the mute action 22 | MediaActionMute MediaAction = "mute" 23 | ) 24 | 25 | // Control sends a media control command 26 | func Control(action MediaAction) error { 27 | return control(action) 28 | } 29 | -------------------------------------------------------------------------------- /utils/handlers/media/media_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package media 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | ) 10 | 11 | func control(action MediaAction) error { 12 | var script string 13 | 14 | switch action { 15 | case MediaActionPlay, MediaActionPause: 16 | script = "tell application \"System Events\" to key code 49" // Space 17 | case MediaActionNext: 18 | script = "tell application \"System Events\" to key code 124" // Right arrow 19 | case MediaActionPrevious: 20 | script = "tell application \"System Events\" to key code 123" // Left arrow 21 | case MediaActionStop: 22 | script = "tell application \"System Events\" to key code 49" // Space (same as play/pause) 23 | case MediaActionVolumeUp: 24 | script = "set volume output volume ((output volume of (get volume settings)) + 5)" 25 | case MediaActionVolumeDown: 26 | script = "set volume output volume ((output volume of (get volume settings)) - 5)" 27 | case MediaActionMute: 28 | script = "set volume with output muted" 29 | default: 30 | return fmt.Errorf("unsupported media action: %s", action) 31 | } 32 | 33 | cmd := exec.Command("osascript", "-e", script) 34 | return cmd.Run() 35 | } 36 | -------------------------------------------------------------------------------- /utils/handlers/media/media_linux.go: -------------------------------------------------------------------------------- 1 | package media 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | ) 7 | 8 | func control(action MediaAction) error { 9 | var cmd *exec.Cmd 10 | 11 | switch action { 12 | case MediaActionPlay, MediaActionPause: 13 | cmd = exec.Command("pavucontrol") 14 | case MediaActionNext: 15 | cmd = exec.Command("pavucontrol", "--next") 16 | case MediaActionPrevious: 17 | cmd = exec.Command("pavucontrol", "--prev") 18 | case MediaActionVolumeUp: 19 | cmd = exec.Command("pavucontrol", "--volume-up") 20 | case MediaActionVolumeDown: 21 | cmd = exec.Command("pavucontrol", "--volume-down") 22 | case MediaActionMute: 23 | cmd = exec.Command("pavucontrol", "--mute") 24 | default: 25 | return fmt.Errorf("unsupported media action: %s", action) 26 | } 27 | 28 | return cmd.Run() 29 | } 30 | -------------------------------------------------------------------------------- /utils/handlers/media/media_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package media 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | ) 10 | 11 | func control(action MediaAction) error { 12 | var script string 13 | 14 | switch action { 15 | case MediaActionPlay, MediaActionPause: 16 | script = "(New-Object -ComObject WScript.Shell).SendKeys([char]179)" 17 | case MediaActionNext: 18 | script = "(New-Object -ComObject WScript.Shell).SendKeys([char]176)" 19 | case MediaActionPrevious: 20 | script = "(New-Object -ComObject WScript.Shell).SendKeys([char]177)" 21 | case MediaActionStop: 22 | script = "(New-Object -ComObject WScript.Shell).SendKeys([char]178)" 23 | case MediaActionVolumeUp: 24 | script = "(New-Object -ComObject WScript.Shell).SendKeys([char]175)" 25 | case MediaActionVolumeDown: 26 | script = "(New-Object -ComObject WScript.Shell).SendKeys([char]174)" 27 | case MediaActionMute: 28 | script = "(New-Object -ComObject WScript.Shell).SendKeys([char]173)" 29 | default: 30 | return fmt.Errorf("unsupported media action: %s", action) 31 | } 32 | 33 | cmd := exec.Command("powershell", "-Command", script) 34 | return cmd.Run() 35 | } 36 | -------------------------------------------------------------------------------- /utils/handlers/notification/notification.go: -------------------------------------------------------------------------------- 1 | package notification 2 | 3 | // NotificationData represents the data needed for a notification 4 | type NotificationData struct { 5 | Title string `json:"title" mapstructure:"title"` 6 | Message string `json:"message" mapstructure:"message"` 7 | Icon string `json:"icon,omitempty" mapstructure:"icon"` 8 | Duration int `json:"duration,omitempty" mapstructure:"duration"` // Duration in milliseconds 9 | } 10 | 11 | // Send sends a notification 12 | func Send(data NotificationData) error { 13 | return send(data) 14 | } 15 | -------------------------------------------------------------------------------- /utils/handlers/notification/notification_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package notification 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | ) 10 | 11 | func send(data NotificationData) error { 12 | script := fmt.Sprintf(`display notification "%s" with title "%s"`, data.Message, data.Title) 13 | if data.Icon != "" { 14 | script = fmt.Sprintf(`%s subtitle "%s"`, script, data.Icon) 15 | } 16 | 17 | cmd := exec.Command("osascript", "-e", script) 18 | return cmd.Run() 19 | } 20 | -------------------------------------------------------------------------------- /utils/handlers/notification/notification_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package notification 5 | 6 | import ( 7 | "os/exec" 8 | "strconv" 9 | ) 10 | 11 | func send(data NotificationData) error { 12 | args := []string{ 13 | "--app-name=System Bridge", 14 | "--urgency=normal", 15 | } 16 | 17 | if data.Title != "" { 18 | args = append(args, "--title="+data.Title) 19 | } 20 | 21 | if data.Icon != "" { 22 | args = append(args, "--icon="+data.Icon) 23 | } 24 | 25 | if data.Duration > 0 { 26 | args = append(args, "--expire-time="+strconv.Itoa(data.Duration)) 27 | } 28 | 29 | args = append(args, data.Message) 30 | 31 | cmd := exec.Command("notify-send", args...) 32 | return cmd.Run() 33 | } 34 | -------------------------------------------------------------------------------- /utils/handlers/notification/notification_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package notification 5 | 6 | import ( 7 | "github.com/go-toast/toast" 8 | ) 9 | 10 | func send(data NotificationData) error { 11 | notification := toast.Notification{ 12 | AppID: "System Bridge", 13 | Title: data.Title, 14 | Message: data.Message, 15 | } 16 | if data.Icon != "" { 17 | notification.Icon = data.Icon 18 | } 19 | if data.Duration > 0 { 20 | // go-toast supports Duration: toast.Short or toast.Long (not ms), so pick based on threshold 21 | if data.Duration >= 7000 { 22 | notification.Duration = toast.Long 23 | } else { 24 | notification.Duration = toast.Short 25 | } 26 | } 27 | return notification.Push() 28 | } 29 | -------------------------------------------------------------------------------- /utils/handlers/power/power.go: -------------------------------------------------------------------------------- 1 | package power 2 | 3 | // Shutdown shuts down the system 4 | func Shutdown() error { 5 | return shutdown() 6 | } 7 | 8 | // Restart restarts the system 9 | func Restart() error { 10 | return restart() 11 | } 12 | 13 | // Sleep puts the system to sleep 14 | func Sleep() error { 15 | return sleep() 16 | } 17 | 18 | // Hibernate hibernates the system 19 | func Hibernate() error { 20 | return hibernate() 21 | } 22 | 23 | // Lock locks the system 24 | func Lock() error { 25 | return lock() 26 | } 27 | 28 | // Logout logs out the current user 29 | func Logout() error { 30 | return logout() 31 | } 32 | -------------------------------------------------------------------------------- /utils/handlers/power/power_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package power 5 | 6 | import ( 7 | "os/exec" 8 | ) 9 | 10 | func shutdown() error { 11 | cmd := exec.Command("osascript", "-e", "tell application \"System Events\" to shut down") 12 | return cmd.Run() 13 | } 14 | 15 | func restart() error { 16 | cmd := exec.Command("osascript", "-e", "tell application \"System Events\" to restart") 17 | return cmd.Run() 18 | } 19 | 20 | func sleep() error { 21 | cmd := exec.Command("osascript", "-e", "tell application \"System Events\" to sleep") 22 | return cmd.Run() 23 | } 24 | 25 | func hibernate() error { 26 | // macOS doesn't have a separate hibernate command, using sleep instead 27 | return sleep() 28 | } 29 | 30 | func lock() error { 31 | cmd := exec.Command("/System/Library/CoreServices/Menu Extras/User.menu/Contents/.resources/CGSession", "-suspend") 32 | return cmd.Run() 33 | } 34 | 35 | func logout() error { 36 | cmd := exec.Command("osascript", "-e", "tell application \"System Events\" to log out") 37 | return cmd.Run() 38 | } 39 | -------------------------------------------------------------------------------- /utils/handlers/power/power_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package power 5 | 6 | import ( 7 | "os/exec" 8 | ) 9 | 10 | func shutdown() error { 11 | cmd := exec.Command("systemctl", "poweroff") 12 | return cmd.Run() 13 | } 14 | 15 | func restart() error { 16 | cmd := exec.Command("systemctl", "reboot") 17 | return cmd.Run() 18 | } 19 | 20 | func sleep() error { 21 | cmd := exec.Command("systemctl", "suspend") 22 | return cmd.Run() 23 | } 24 | 25 | func hibernate() error { 26 | cmd := exec.Command("systemctl", "hibernate") 27 | return cmd.Run() 28 | } 29 | 30 | func lock() error { 31 | cmd := exec.Command("loginctl", "lock-session") 32 | return cmd.Run() 33 | } 34 | 35 | func logout() error { 36 | cmd := exec.Command("loginctl", "terminate-session", "$XDG_SESSION_ID") 37 | return cmd.Run() 38 | } 39 | -------------------------------------------------------------------------------- /utils/handlers/power/power_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package power 5 | 6 | import ( 7 | "os/exec" 8 | ) 9 | 10 | func shutdown() error { 11 | cmd := exec.Command("shutdown", "/s") 12 | return cmd.Run() 13 | } 14 | 15 | func restart() error { 16 | cmd := exec.Command("shutdown", "/r") 17 | return cmd.Run() 18 | } 19 | 20 | func sleep() error { 21 | cmd := exec.Command("powercfg", "-hibernate", "off") 22 | if err := cmd.Run(); err != nil { 23 | return err 24 | } 25 | cmd = exec.Command("rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0") 26 | return cmd.Run() 27 | } 28 | 29 | func hibernate() error { 30 | cmd := exec.Command("powercfg", "-hibernate", "on") 31 | if err := cmd.Run(); err != nil { 32 | return err 33 | } 34 | cmd = exec.Command("rundll32.exe", "powrprof.dll,SetSuspendState", "1,1,0") 35 | return cmd.Run() 36 | } 37 | 38 | func lock() error { 39 | cmd := exec.Command("rundll32.exe", "user32.dll,LockWorkStation") 40 | return cmd.Run() 41 | } 42 | 43 | func logout() error { 44 | cmd := exec.Command("shutdown", "/l") 45 | return cmd.Run() 46 | } 47 | -------------------------------------------------------------------------------- /utils/handlers/settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "github.com/timmo001/system-bridge/settings" 5 | ) 6 | 7 | // Settings represents the application settings 8 | type Settings = settings.Settings 9 | 10 | // Load loads the settings from disk 11 | func Load() (*Settings, error) { 12 | return settings.Load() 13 | } 14 | 15 | // Save saves the settings to disk 16 | func Save(settings *Settings) error { 17 | return settings.Save() 18 | } 19 | 20 | // Update updates the settings with new values 21 | func Update(current *Settings, new *Settings) error { 22 | current.API = new.API 23 | current.Autostart = new.Autostart 24 | current.Hotkeys = new.Hotkeys 25 | current.LogLevel = new.LogLevel 26 | current.Media = new.Media 27 | return current.Save() 28 | } 29 | -------------------------------------------------------------------------------- /utils/path.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | ) 9 | 10 | // GetConfigPath returns the path to the config directory 11 | func GetConfigPath() (string, error) { 12 | var configDirPath string 13 | 14 | switch runtime.GOOS { 15 | case "windows": 16 | // First check if user has explicitly set a config path 17 | if customPath := os.Getenv("SYSTEM_BRIDGE_CONFIG_DIR"); customPath != "" { 18 | configDirPath = customPath 19 | } else if appData := os.Getenv("LOCALAPPDATA"); appData != "" { 20 | configDirPath = filepath.Join(appData, "system-bridge", "v5") 21 | } else { 22 | return "", fmt.Errorf("LOCALAPPDATA environment variable not set") 23 | } 24 | case "darwin": 25 | // First check if user has explicitly set a config path 26 | if customPath := os.Getenv("SYSTEM_BRIDGE_CONFIG_DIR"); customPath != "" { 27 | configDirPath = customPath 28 | } else if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" { 29 | // Support XDG spec on macOS for CLI apps 30 | configDirPath = filepath.Join(xdgData, "system-bridge", "v5") 31 | } else if home := os.Getenv("HOME"); home != "" { 32 | configDirPath = filepath.Join(home, "Library", "Application Support", "system-bridge", "v5") 33 | } else { 34 | return "", fmt.Errorf("HOME environment variable not set") 35 | } 36 | default: 37 | // Linux and others: Follow XDG Base Directory Specification 38 | if customPath := os.Getenv("SYSTEM_BRIDGE_CONFIG_DIR"); customPath != "" { 39 | configDirPath = customPath 40 | } else if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" { 41 | configDirPath = filepath.Join(xdgData, "system-bridge", "v5") 42 | } else if home := os.Getenv("HOME"); home != "" { 43 | configDirPath = filepath.Join(home, ".local", "share", "system-bridge", "v5") 44 | } else { 45 | return "", fmt.Errorf("HOME environment variable not set") 46 | } 47 | } 48 | 49 | // Ensure the path is absolute 50 | if !filepath.IsAbs(configDirPath) { 51 | return "", fmt.Errorf("config directory path must be absolute") 52 | } 53 | 54 | // Clean the path and create the config directory if it doesn't exist 55 | configDirPath = filepath.Clean(configDirPath) 56 | if err := os.MkdirAll(configDirPath, 0755); err != nil { 57 | return "", fmt.Errorf("could not create config directory: %w", err) 58 | } 59 | 60 | return configDirPath, nil 61 | } 62 | 63 | // GetDataPath returns the path to the data directory 64 | func GetDataPath() (string, error) { 65 | configPath, err := GetConfigPath() 66 | if err != nil { 67 | return "", err 68 | } 69 | 70 | dataPath := filepath.Join(configPath, "data") 71 | dataPath = filepath.Clean(dataPath) 72 | 73 | // Create the data directory if it doesn't exist 74 | if err := os.MkdirAll(dataPath, 0755); err != nil { 75 | return "", fmt.Errorf("could not create data directory: %w", err) 76 | } 77 | 78 | return dataPath, nil 79 | } -------------------------------------------------------------------------------- /utils/token.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/google/uuid" 4 | 5 | func GenerateToken() string { 6 | return uuid.New().String() 7 | } 8 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/timmo001/system-bridge/utils/http" 10 | ) 11 | 12 | var ( 13 | // Version is the current version of System Bridge 14 | // This is set via ldflags during build: 15 | // For tags: The tag name (e.g. v5.0.0) 16 | // For development: 5.0.0-dev+[commit-sha] 17 | Version string = "5.0.0" 18 | 19 | // LatestVersionURL is the URL to check for the latest version 20 | LatestVersionURL = "https://api.github.com/repos/timmo001/system-bridge/releases/latest" 21 | 22 | // LatestVersionUserURL is the URL to check for the latest version for the user 23 | LatestVersionUserURL = "https://github.com/timmo001/system-bridge/releases/latest" 24 | 25 | // HTTP client with caching and rate limiting 26 | client = http.NewClient(&http.ClientConfig{ 27 | DefaultTTL: 5 * time.Minute, 28 | MaxRequests: 30, // Conservative limit for GitHub's unauthenticated API 29 | TimeWindow: time.Hour, 30 | }) 31 | ) 32 | 33 | // GetLatestVersion fetches the latest version from GitHub with caching 34 | func GetLatestVersion() (string, error) { 35 | body, err := client.Get(LatestVersionURL) 36 | if err != nil { 37 | return "", fmt.Errorf("failed to fetch latest version: %v", err) 38 | } 39 | 40 | var release struct { 41 | TagName string `json:"tag_name"` 42 | } 43 | if err := json.Unmarshal(body, &release); err != nil { 44 | return "", fmt.Errorf("failed to parse response: %v", err) 45 | } 46 | 47 | // Remove 'v' prefix if present 48 | version := strings.TrimPrefix(release.TagName, "v") 49 | 50 | // TODO: Remove this check once version 5.0.0 is officially released 51 | // Always ensure we don't return a version lower than 5.0.0 52 | if !IsNewerVersionAvailable("5.0.0", version) { 53 | return "5.0.0", nil 54 | } 55 | return version, nil 56 | } 57 | 58 | // IsNewerVersionAvailable checks if a newer version is available 59 | func IsNewerVersionAvailable(currentVersion, latestVersion string) bool { 60 | // Remove 'v' prefix if present 61 | current := strings.TrimPrefix(currentVersion, "v") 62 | latest := strings.TrimPrefix(latestVersion, "v") 63 | 64 | // Split versions into parts 65 | currentParts := strings.Split(current, ".") 66 | latestParts := strings.Split(latest, ".") 67 | 68 | // Compare major, minor, and patch versions 69 | for i := range 3 { 70 | if i >= len(currentParts) { 71 | return true 72 | } 73 | if i >= len(latestParts) { 74 | return false 75 | } 76 | 77 | currentNum := 0 78 | latestNum := 0 79 | if _, err := fmt.Sscanf(currentParts[i], "%d", ¤tNum); err != nil { 80 | currentNum = 0 81 | } 82 | if _, err := fmt.Sscanf(latestParts[i], "%d", &latestNum); err != nil { 83 | latestNum = 0 84 | } 85 | 86 | if latestNum > currentNum { 87 | return true 88 | } 89 | if currentNum > latestNum { 90 | return false 91 | } 92 | } 93 | 94 | return false 95 | } 96 | -------------------------------------------------------------------------------- /web-client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | db.sqlite 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | next-env.d.ts 20 | 21 | # production 22 | /build 23 | 24 | # misc 25 | .DS_Store 26 | *.pem 27 | 28 | # debug 29 | npm-debug.log* 30 | yarn-debug.log* 31 | yarn-error.log* 32 | .pnpm-debug.log* 33 | 34 | # local env files 35 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 36 | .env 37 | .env*.local 38 | 39 | # vercel 40 | .vercel 41 | 42 | # typescript 43 | *.tsbuildinfo 44 | 45 | # idea files 46 | .idea 47 | 48 | # system-bridge 49 | !system-bridge 50 | -------------------------------------------------------------------------------- /web-client/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "~/components", 15 | "utils": "~/lib/utils", 16 | "ui": "~/components/ui", 17 | "lib": "~/lib", 18 | "hooks": "~/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /web-client/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from "@eslint/eslintrc"; 2 | import tseslint from "typescript-eslint"; 3 | 4 | const compat = new FlatCompat({ 5 | baseDirectory: import.meta.dirname, 6 | }); 7 | 8 | export default tseslint.config( 9 | { 10 | ignores: [".next"], 11 | }, 12 | ...compat.extends("next/core-web-vitals"), 13 | { 14 | files: ["**/*.ts", "**/*.tsx"], 15 | extends: [ 16 | ...tseslint.configs.recommended, 17 | ...tseslint.configs.recommendedTypeChecked, 18 | ...tseslint.configs.stylisticTypeChecked, 19 | ], 20 | rules: { 21 | "@typescript-eslint/array-type": "off", 22 | "@typescript-eslint/consistent-type-definitions": "off", 23 | "@typescript-eslint/consistent-type-imports": [ 24 | "warn", 25 | { prefer: "type-imports", fixStyle: "inline-type-imports" }, 26 | ], 27 | "@typescript-eslint/no-unused-vars": [ 28 | "warn", 29 | { argsIgnorePattern: "^_" }, 30 | ], 31 | "@typescript-eslint/require-await": "off", 32 | "@typescript-eslint/no-misused-promises": [ 33 | "error", 34 | { checksVoidReturn: { attributes: false } }, 35 | ], 36 | }, 37 | }, 38 | { 39 | linterOptions: { 40 | reportUnusedDisableDirectives: true, 41 | }, 42 | languageOptions: { 43 | parserOptions: { 44 | projectService: true, 45 | }, 46 | }, 47 | }, 48 | ); 49 | -------------------------------------------------------------------------------- /web-client/next.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | import "./src/env.js"; 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const nextConfig = { 9 | experimental: { 10 | reactCompiler: true, 11 | }, 12 | eslint: { 13 | ignoreDuringBuilds: true, 14 | }, 15 | reactStrictMode: true, 16 | output: process.env.STATIC_EXPORT ? "export" : undefined, 17 | }; 18 | 19 | if (!process.env.STATIC_EXPORT) { 20 | nextConfig.rewrites = async () => { 21 | return [ 22 | { 23 | source: "/app/settings.html", 24 | destination: "/settings", 25 | }, 26 | { 27 | source: "/app/data.html", 28 | destination: "/data", 29 | }, 30 | ]; 31 | }; 32 | } 33 | 34 | export default nextConfig; 35 | -------------------------------------------------------------------------------- /web-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "system-bridge-web-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "next build", 8 | "check": "next lint && tsc --noEmit", 9 | "dev": "next dev --turbo", 10 | "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", 11 | "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", 12 | "lint": "next lint", 13 | "lint:fix": "next lint --fix", 14 | "preview": "next build && next start", 15 | "start": "next start", 16 | "typecheck": "tsc --noEmit" 17 | }, 18 | "dependencies": { 19 | "@hookform/resolvers": "^5.0.1", 20 | "@radix-ui/react-dropdown-menu": "^2.1.14", 21 | "@radix-ui/react-label": "^2.1.6", 22 | "@radix-ui/react-select": "^2.2.4", 23 | "@radix-ui/react-slot": "^1.2.2", 24 | "@radix-ui/react-switch": "^1.2.4", 25 | "@radix-ui/react-tabs": "^1.1.11", 26 | "@t3-oss/env-nextjs": "^0.13.4", 27 | "babel-plugin-react-compiler": "^19.1.0-rc.1", 28 | "class-variance-authority": "^0.7.1", 29 | "clsx": "^2.1.1", 30 | "lucide-react": "^0.513.0", 31 | "next": "^15.3.2", 32 | "next-themes": "^0.4.6", 33 | "nuqs": "^2.4.3", 34 | "react": "^19.1.0", 35 | "react-dom": "^19.1.0", 36 | "react-hook-form": "^7.56.3", 37 | "react-markdown": "^10.1.0", 38 | "rehype-prism-plus": "^2.0.1", 39 | "rehype-raw": "^7.0.0", 40 | "rehype-slug": "^6.0.0", 41 | "remark-gfm": "^4.0.1", 42 | "sonner": "^2.0.3", 43 | "tailwind-merge": "^3.2.0", 44 | "tw-animate-css": "^1.2.9", 45 | "zod": "^3.24.4", 46 | "zustand": "^5.0.4" 47 | }, 48 | "devDependencies": { 49 | "@eslint/eslintrc": "^3.3.1", 50 | "@tailwindcss/postcss": "^4.1.5", 51 | "@types/node": "^22.15.17", 52 | "@types/react": "^19.1.3", 53 | "@types/react-dom": "^19.1.3", 54 | "eslint": "^9.26.0", 55 | "eslint-config-next": "^15.3.2", 56 | "postcss": "^8.5.3", 57 | "prettier": "^3.5.3", 58 | "prettier-plugin-tailwindcss": "^0.6.11", 59 | "tailwindcss": "^4.1.5", 60 | "typescript": "^5.8.3", 61 | "typescript-eslint": "^8.32.0" 62 | }, 63 | "ct3aMetadata": { 64 | "initVersion": "7.39.2" 65 | } 66 | } -------------------------------------------------------------------------------- /web-client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /web-client/prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */ 2 | export default { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | }; 5 | -------------------------------------------------------------------------------- /web-client/src/app/(websocket)/(client)/data/_components/data.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Modules } from "~/lib/system-bridge/types-modules"; 4 | import { CodeBlock } from "~/components/ui/code-block"; 5 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; 6 | import { useSystemBridgeWS } from "~/components/hooks/use-system-bridge-ws"; 7 | 8 | export function Data() { 9 | const { data } = useSystemBridgeWS(); 10 | 11 | return ( 12 | 13 | 14 | {Modules.map((module) => ( 15 | 16 | {module.charAt(0).toUpperCase() + module.slice(1)} 17 | 18 | ))} 19 | 20 | {Modules.map((module) => ( 21 | 22 | 23 | {JSON.stringify(data?.[module], null, 2)} 24 | 25 | 26 | ))} 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /web-client/src/app/(websocket)/(client)/data/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next"; 2 | 3 | import { Data } from "~/app/(websocket)/(client)/data/_components/data"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Data", 7 | description: "Data for System Bridge", 8 | }; 9 | 10 | export default function DataPage() { 11 | return ( 12 | <> 13 |

Data

14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /web-client/src/app/(websocket)/(client)/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSystemBridgeWS } from "~/components/hooks/use-system-bridge-ws"; 4 | 5 | export default function WebSocketClientLayout({ 6 | children, 7 | }: Readonly<{ children: React.ReactNode }>) { 8 | const { isConnected } = useSystemBridgeWS(); 9 | 10 | return <>{!isConnected ?
Connecting...
: <>{children}}; 11 | } 12 | -------------------------------------------------------------------------------- /web-client/src/app/(websocket)/(client)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next"; 2 | 3 | import { Settings } from "~/app/(websocket)/(client)/settings/_components/settings"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Settings", 7 | description: "Settings for System Bridge", 8 | }; 9 | 10 | export default function SettingsPage() { 11 | return ( 12 | <> 13 |

Settings

14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /web-client/src/app/(websocket)/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { 5 | parseAsBoolean, 6 | parseAsInteger, 7 | parseAsString, 8 | useQueryState, 9 | } from "nuqs"; 10 | 11 | import { SetupConnection } from "~/components/setup-connection"; 12 | import { useSystemBridgeConnectionStore } from "~/components/hooks/use-system-bridge-connection"; 13 | import { SystemBridgeWSProvider } from "~/components/providers/system-bridge-ws-provider"; 14 | 15 | export default function WebSocketLayout({ 16 | children, 17 | }: Readonly<{ children: React.ReactNode }>) { 18 | const [hostInput] = useQueryState("host", parseAsString); 19 | const [portInput] = useQueryState("port", parseAsInteger); 20 | const [apiKeyInput] = useQueryState("apiKey", parseAsString); 21 | const [sslInput] = useQueryState("ssl", parseAsBoolean.withDefault(false)); 22 | 23 | const [isHydrated, setIsHydrated] = useState(false); 24 | 25 | const { host, port, token, setAll } = useSystemBridgeConnectionStore(); 26 | 27 | useEffect(() => { 28 | if (hostInput && portInput && apiKeyInput) { 29 | void setAll({ 30 | host: hostInput, 31 | port: portInput, 32 | ssl: sslInput, 33 | token: apiKeyInput, 34 | }).then(() => { 35 | setIsHydrated(true); 36 | }); 37 | } else { 38 | setIsHydrated(true); 39 | } 40 | }, [apiKeyInput, hostInput, portInput, setAll, sslInput]); 41 | 42 | return ( 43 | 44 | {!isHydrated ? ( 45 |
Loading...
46 | ) : !host || !port || !token ? ( 47 | 48 | ) : ( 49 | <>{children} 50 | )} 51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /web-client/src/app/(websocket)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { parseAsInteger, parseAsString, useQueryState } from "nuqs"; 3 | 4 | import { ButtonLink } from "~/components/ui/button-link"; 5 | import { useSystemBridgeConnectionStore } from "~/components/hooks/use-system-bridge-connection"; 6 | 7 | export default function HomePage() { 8 | const [hostInput] = useQueryState("host", parseAsString); 9 | const [portInput] = useQueryState("port", parseAsInteger); 10 | const [apiKeyInput] = useQueryState("apiKey", parseAsString); 11 | 12 | const { host, port, token, ssl } = useSystemBridgeConnectionStore(); 13 | 14 | return ( 15 | <> 16 |

Welcome to System Bridge

17 | 18 |
19 | 20 | 25 |
26 | 27 |
28 | {hostInput && portInput && apiKeyInput ? ( 29 | <> 30 | ) : ( 31 | 36 | )} 37 |

38 | 39 | Your connection details 40 | 41 |
42 | Host: 43 |
44 | {host} 45 |
46 |
47 | Port: 48 |
49 | {port} 50 |
51 |
52 | API Key: 53 |
54 | {token} 55 |
56 |
57 | SSL: 58 |
59 | {ssl ? "Yes" : "No"} 60 |

61 |
62 | 63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /web-client/src/app/connection/_components/connection.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useEffect, useState } from "react"; 3 | 4 | import { SetupConnection } from "~/components/setup-connection"; 5 | 6 | export function Connection() { 7 | const [isHydrated, setIsHydrated] = useState(false); 8 | 9 | useEffect(() => { 10 | setIsHydrated(true); 11 | }, []); 12 | 13 | return ( 14 | <> 15 | {!isHydrated ? ( 16 |
Loading...
17 | ) : ( 18 | <> 19 |

Connection Settings

20 | 21 | 22 | )} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /web-client/src/app/connection/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from "next"; 2 | 3 | import { Connection } from "~/app/connection/_components/connection"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Connection", 7 | description: "Connection settings for System Bridge", 8 | }; 9 | 10 | export default function ConnectionPage() { 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /web-client/src/app/icon.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from "next/og"; 2 | 3 | // Image metadata 4 | export const size = { 5 | width: 32, 6 | height: 32, 7 | }; 8 | export const contentType = "image/png"; 9 | export const dynamic = "force-static"; 10 | 11 | // Image generation 12 | export default function Icon() { 13 | return new ImageResponse( 14 | ( 15 | // ImageResponse JSX element 16 |
28 | SB 29 |
30 | ), 31 | // ImageResponse options 32 | { 33 | // For convenience, we can re-use the exported icons size metadata 34 | // config to also set the ImageResponse's width and height. 35 | ...size, 36 | }, 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /web-client/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "~/styles/globals.css"; 2 | 3 | import { type Metadata } from "next"; 4 | import Link from "next/link"; 5 | import { Geist } from "next/font/google"; 6 | import { NuqsAdapter } from "nuqs/adapters/next/app"; 7 | 8 | import { Toaster } from "~/components/ui/sonner"; 9 | import { ThemeProvider } from "~/components/providers/theme-provider"; 10 | import { ThemeToggle } from "~/components/theme-toggle"; 11 | import { Suspense } from "react"; 12 | 13 | export const metadata: Metadata = { 14 | title: { 15 | default: "System Bridge Client", 16 | template: "%s | System Bridge Client", 17 | }, 18 | description: "System Bridge Client", 19 | icons: [{ rel: "icon", url: "/icon" }], 20 | robots: { 21 | index: false, 22 | follow: false, 23 | }, 24 | }; 25 | 26 | const geist = Geist({ 27 | subsets: ["latin"], 28 | variable: "--font-geist-sans", 29 | }); 30 | 31 | export default function RootLayout({ 32 | children, 33 | }: Readonly<{ children: React.ReactNode }>) { 34 | return ( 35 | 36 | 37 | 43 |
44 |
45 | 46 |

System Bridge

47 | 48 | 49 |
50 |
51 |
52 | 53 | Loading...}>{children} 54 | 55 |
56 | 57 |
58 | 59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /web-client/src/components/hooks/use-system-bridge-connection.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { create } from "zustand"; 3 | import { persist, createJSONStorage } from "zustand/middleware"; 4 | 5 | interface SystemBridgeConnectionState { 6 | host: string; 7 | port: number; 8 | ssl: boolean; 9 | token: string | null; 10 | setAll: (state: { 11 | host: string; 12 | port: number; 13 | ssl: boolean; 14 | token: string | null; 15 | }) => Promise; 16 | setHost: (host: string) => void; 17 | setPort: (port: number) => void; 18 | setSsl: (ssl: boolean) => void; 19 | setToken: (token: string | null) => void; 20 | } 21 | 22 | export const useSystemBridgeConnectionStore = create()( 23 | persist( 24 | (set) => ({ 25 | host: "0.0.0.0", 26 | port: 9170, 27 | ssl: false, 28 | token: null, 29 | setAll: async ({ 30 | host, 31 | port, 32 | ssl, 33 | token, 34 | }: { 35 | host: string; 36 | port: number; 37 | ssl: boolean; 38 | token: string | null; 39 | }) => { 40 | set({ host, port, ssl, token }); 41 | return Promise.resolve(); 42 | }, 43 | setHost: (host: string) => set({ host }), 44 | setPort: (port: number) => set({ port }), 45 | setSsl: (ssl: boolean) => set({ ssl }), 46 | setToken: (token: string | null) => set({ token }), 47 | }), 48 | { 49 | name: "system-bridge-connection", 50 | storage: createJSONStorage(() => localStorage), 51 | }, 52 | ), 53 | ); 54 | -------------------------------------------------------------------------------- /web-client/src/components/hooks/use-system-bridge-ws.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useContext } from "react"; 3 | 4 | import { SystemBridgeWSContext } from "~/components/providers/system-bridge-ws-provider"; 5 | 6 | export function useSystemBridgeWS() { 7 | const context = useContext(SystemBridgeWSContext); 8 | if (context === undefined) { 9 | throw new Error( 10 | "useSystemBridgeWS must be used within a SystemBridgeWSProvider", 11 | ); 12 | } 13 | return context; 14 | } 15 | -------------------------------------------------------------------------------- /web-client/src/components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { 5 | type ThemeProviderProps, 6 | ThemeProvider as NextThemesProvider, 7 | } from "next-themes"; 8 | 9 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /web-client/src/components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Moon, Sun } from "lucide-react"; 5 | import { useTheme } from "next-themes"; 6 | 7 | import { Button } from "~/components/ui/button"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from "~/components/ui/dropdown-menu"; 14 | 15 | export function ThemeToggle() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /web-client/src/components/ui/button-link.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { Button } from "~/components/ui/button"; 4 | 5 | export function ButtonLink({ 6 | buttonClassName, 7 | linkClassName, 8 | title, 9 | href, 10 | }: { 11 | buttonClassName?: string; 12 | linkClassName?: string; 13 | title: string; 14 | href: string; 15 | }) { 16 | return ( 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /web-client/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "~/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | }, 36 | ); 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean; 47 | }) { 48 | const Comp = asChild ? Slot : "button"; 49 | 50 | return ( 51 | 56 | ); 57 | } 58 | 59 | export { Button, buttonVariants }; 60 | -------------------------------------------------------------------------------- /web-client/src/components/ui/callout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AlertCircleIcon, 3 | AlertTriangleIcon, 4 | InfoIcon, 5 | LightbulbIcon, 6 | } from "lucide-react"; 7 | 8 | import { cn } from "~/lib/utils"; 9 | 10 | export type CalloutProps = React.HTMLAttributes & { 11 | type?: "info" | "warning" | "success" | "error"; 12 | }; 13 | 14 | export function Callout({ 15 | className, 16 | type = "info", 17 | children, 18 | ...props 19 | }: CalloutProps) { 20 | const icons = { 21 | info: InfoIcon, 22 | warning: AlertTriangleIcon, 23 | success: LightbulbIcon, 24 | error: AlertCircleIcon, 25 | }; 26 | 27 | const Icon = icons[type]; 28 | 29 | const borderColors = { 30 | info: "border-l-blue-600 dark:border-l-blue-600", 31 | warning: "border-l-yellow-700 dark:border-l-yellow-600", 32 | success: "border-l-green-600 dark:border-l-green-500", 33 | error: "border-l-red-600 dark:border-l-red-500", 34 | }; 35 | 36 | const iconColors = { 37 | info: "text-blue-600 dark:text-blue-400", 38 | warning: "text-yellow-700 dark:text-yellow-400", 39 | success: "text-green-600 dark:text-green-400", 40 | error: "text-red-600 dark:text-red-400", 41 | }; 42 | 43 | const backgroundColors = { 44 | info: "bg-blue-50/50 dark:bg-blue-950/30", 45 | warning: "bg-yellow-50/50 dark:bg-yellow-950/30", 46 | success: "bg-green-50/50 dark:bg-green-950/30", 47 | error: "bg-red-50/50 dark:bg-red-950/30", 48 | }; 49 | 50 | const alertTitle = { 51 | info: "Note", 52 | warning: "Warning", 53 | success: "Tip", 54 | error: "Caution", 55 | }; 56 | 57 | return ( 58 |
66 |
67 | {/* Left colored border */} 68 |
69 | 70 | {/* Content area with icon and text */} 71 |
72 |
73 | 74 |
75 |
76 |

77 | {alertTitle[type]} 78 |

79 |
80 | {children} 81 |
82 |
83 |
84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /web-client/src/components/ui/code-block.tsx: -------------------------------------------------------------------------------- 1 | import { CopyIcon } from "lucide-react"; 2 | import { toast } from "sonner"; 3 | 4 | import { Button } from "~/components/ui/button"; 5 | import { cn } from "~/lib/utils"; 6 | 7 | export function CodeBlock({ 8 | children, 9 | className, 10 | language, 11 | }: { 12 | children: string; 13 | className?: string; 14 | language?: string; 15 | }) { 16 | const copyToClipboard = (e: React.MouseEvent) => { 17 | e.stopPropagation(); 18 | void navigator.clipboard.writeText(children); 19 | toast.success("Code copied to clipboard"); 20 | }; 21 | 22 | return ( 23 |
24 |
e.stopPropagation()} 27 | > 28 |
29 | 30 | {language ?? "plaintext"} 31 | 32 | 40 |
41 |
42 |
43 |
44 |
45 |             
51 |               {children}
52 |             
53 |           
54 |
55 |
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /web-client/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "~/lib/utils"; 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ); 19 | } 20 | 21 | export { Input }; 22 | -------------------------------------------------------------------------------- /web-client/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | 6 | import { cn } from "~/lib/utils"; 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /web-client/src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | import { Toaster as Sonner, type ToasterProps } from "sonner"; 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme(); 8 | 9 | return ( 10 | 22 | ); 23 | }; 24 | 25 | export { Toaster }; 26 | -------------------------------------------------------------------------------- /web-client/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitive from "@radix-ui/react-switch"; 5 | 6 | import { cn } from "~/lib/utils"; 7 | 8 | function Switch({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | 27 | 28 | ); 29 | } 30 | 31 | export { Switch }; 32 | -------------------------------------------------------------------------------- /web-client/src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 5 | 6 | import { cn } from "~/lib/utils"; 7 | 8 | function Tabs({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ); 19 | } 20 | 21 | function TabsList({ 22 | className, 23 | ...props 24 | }: React.ComponentProps) { 25 | return ( 26 | 34 | ); 35 | } 36 | 37 | function TabsTrigger({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ); 51 | } 52 | 53 | function TabsContent({ 54 | className, 55 | ...props 56 | }: React.ComponentProps) { 57 | return ( 58 | 63 | ); 64 | } 65 | 66 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 67 | -------------------------------------------------------------------------------- /web-client/src/components/ui/typography.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "~/lib/utils"; 2 | 3 | type HeadingProps = React.HTMLAttributes & { 4 | withBorder?: boolean; 5 | }; 6 | 7 | export function H1({ className, withBorder, ...props }: HeadingProps) { 8 | return ( 9 |

17 | ); 18 | } 19 | 20 | export function H2({ className, withBorder, ...props }: HeadingProps) { 21 | return ( 22 |

30 | ); 31 | } 32 | 33 | export function H3({ className, withBorder, ...props }: HeadingProps) { 34 | return ( 35 |

43 | ); 44 | } 45 | 46 | export function H4({ className, withBorder, ...props }: HeadingProps) { 47 | return ( 48 |

56 | ); 57 | } 58 | 59 | type ParagraphProps = React.HTMLAttributes; 60 | 61 | export function P({ className, ...props }: ParagraphProps) { 62 | return ( 63 |

70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /web-client/src/env.js: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | /** 6 | * Specify your server-side environment variables schema here. This way you can ensure the app 7 | * isn't built with invalid env vars. 8 | */ 9 | server: { 10 | NODE_ENV: z.enum(["development", "test", "production"]), 11 | }, 12 | 13 | /** 14 | * Specify your client-side environment variables schema here. This way you can ensure the app 15 | * isn't built with invalid env vars. To expose them to the client, prefix them with 16 | * `NEXT_PUBLIC_`. 17 | */ 18 | client: { 19 | // NEXT_PUBLIC_CLIENTVAR: z.string(), 20 | }, 21 | 22 | /** 23 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. 24 | * middlewares) or client-side so we need to destruct manually. 25 | */ 26 | runtimeEnv: { 27 | NODE_ENV: process.env.NODE_ENV, 28 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, 29 | }, 30 | /** 31 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially 32 | * useful for Docker builds. 33 | */ 34 | skipValidation: !!process.env.SKIP_ENV_VALIDATION, 35 | /** 36 | * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and 37 | * `SOME_VAR=''` will throw an error. 38 | */ 39 | emptyStringAsUndefined: true, 40 | }); 41 | -------------------------------------------------------------------------------- /web-client/src/lib/system-bridge/types-modules.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const Modules = [ 4 | "battery", 5 | "cpu", 6 | "disks", 7 | "displays", 8 | "gpus", 9 | "media", 10 | "memory", 11 | "networks", 12 | "processes", 13 | "sensors", 14 | "system", 15 | ] as const; 16 | 17 | export const ModuleNameSchema = z.enum(Modules); 18 | 19 | export type ModuleName = z.infer; 20 | 21 | export const ModuleDataSchema = z.record(ModuleNameSchema, z.any()); 22 | 23 | export type ModuleData = z.infer; 24 | 25 | export const DefaultModuleData: ModuleData = Modules.reduce((acc, module) => { 26 | acc[module] = {}; 27 | return acc; 28 | }, {} as ModuleData); 29 | -------------------------------------------------------------------------------- /web-client/src/lib/system-bridge/types-settings.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const SettingsAPISchema = z.object({ 4 | token: z.string(), 5 | port: z.number(), 6 | }); 7 | 8 | export type SettingsAPI = z.infer; 9 | 10 | export const SettingsHotkeySchema = z.object({ 11 | name: z.string(), 12 | key: z.string(), 13 | }); 14 | 15 | export type SettingsHotkey = z.infer; 16 | 17 | export const SettingsMediaDirectorySchema = z.object({ 18 | name: z.string(), 19 | path: z.string(), 20 | }); 21 | 22 | export type SettingsMediaDirectory = z.infer< 23 | typeof SettingsMediaDirectorySchema 24 | >; 25 | 26 | export const SettingsMediaSchema = z.object({ 27 | directories: z.array(SettingsMediaDirectorySchema), 28 | }); 29 | 30 | export type SettingsMedia = z.infer; 31 | 32 | export const SettingsSchema = z.object({ 33 | api: SettingsAPISchema, 34 | autostart: z.boolean(), 35 | hotkeys: z.array(SettingsHotkeySchema), 36 | logLevel: z.enum(["debug", "info", "warn", "error"]), 37 | media: SettingsMediaSchema, 38 | }); 39 | 40 | export type Settings = z.infer; 41 | -------------------------------------------------------------------------------- /web-client/src/lib/system-bridge/types-websocket.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { ModuleNameSchema } from "~/lib/system-bridge/types-modules"; 4 | 5 | export const EventTypeSchema = z.enum([ 6 | "EXIT_APPLICATION", 7 | "GET_DATA", 8 | "GET_DIRECTORIES", 9 | "GET_DIRECTORY", 10 | "GET_FILES", 11 | "GET_FILE", 12 | "GET_SETTINGS", 13 | "KEYBOARD_KEYPRESS", 14 | "KEYBOARD_TEXT", 15 | "MEDIA_CONTROL", 16 | "NOTIFICATION", 17 | "OPEN", 18 | "POWER_HIBERNATE", 19 | "POWER_LOCK", 20 | "POWER_LOGOUT", 21 | "POWER_RESTART", 22 | "POWER_SHUTDOWN", 23 | "POWER_SLEEP", 24 | "REGISTER_DATA_LISTENER", 25 | "UNREGISTER_DATA_LISTENER", 26 | "DATA_UPDATE", 27 | "UPDATE_SETTINGS", 28 | ]); 29 | 30 | export type EventType = z.infer; 31 | 32 | export const ResponseTypeSchema = z.enum([ 33 | "ERROR", 34 | "APPLICATION_EXITING", 35 | "DATA_GET", 36 | "DIRECTORIES", 37 | "DIRECTORY", 38 | "FILES", 39 | "FILE", 40 | "KEYBOARD_KEY_PRESSED", 41 | "KEYBOARD_TEXT_SENT", 42 | "MEDIA_CONTROLLED", 43 | "NOTIFICATION_SENT", 44 | "OPENED", 45 | "POWER_HIBERNATING", 46 | "POWER_LOCKING", 47 | "POWER_LOGGINGOUT", 48 | "POWER_RESTARTING", 49 | "POWER_SHUTTINGDOWN", 50 | "POWER_SLEEPING", 51 | "DATA_LISTENER_REGISTERED", 52 | "DATA_LISTENER_UNREGISTERED", 53 | "DATA_UPDATE", 54 | "SETTINGS_RESULT", 55 | "SETTINGS_UPDATED", 56 | ]); 57 | 58 | export type ResponseType = z.infer; 59 | 60 | export const ResponseSubtypeSchema = z.enum([ 61 | "NONE", 62 | "BAD_REQUEST", 63 | "BAD_TOKEN", 64 | "BAD_JSON", 65 | "BAD_DIRECTORY", 66 | "BAD_FILE", 67 | "BAD_PATH", 68 | "INVALID_ACTION", 69 | "LISTENER_ALREADY_REGISTERED", 70 | "LISTENER_NOT_REGISTERED", 71 | "MISSING_ACTION", 72 | "MISSING_BASE", 73 | "MISSING_KEY", 74 | "MISSING_MODULES", 75 | "MISSING_PATH", 76 | "MISSING_PATH_URL", 77 | "MISSING_SETTING", 78 | "MISSING_TEXT", 79 | "MISSING_TITLE", 80 | "MISSING_TOKEN", 81 | "MISSING_VALUE", 82 | "UNKNOWN_EVENT", 83 | ]); 84 | 85 | export type ResponseSubtype = z.infer; 86 | 87 | export const WebSocketRequestSchema = z.object({ 88 | id: z.string(), 89 | event: EventTypeSchema, 90 | data: z.any().default({}), 91 | token: z.string(), 92 | }); 93 | 94 | export type WebSocketRequest = z.infer; 95 | 96 | export const WebSocketResponseSchema = z.object({ 97 | id: z.string(), 98 | type: ResponseTypeSchema, 99 | subtype: ResponseSubtypeSchema, 100 | data: z.any(), 101 | message: z.string().optional(), 102 | module: ModuleNameSchema.optional(), 103 | }); 104 | 105 | export type MessageResponse = z.infer; 106 | -------------------------------------------------------------------------------- /web-client/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function generateUUID() { 9 | return crypto.randomUUID(); 10 | } 11 | -------------------------------------------------------------------------------- /web-client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Base Options: */ 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "verbatimModuleSyntax": true, 12 | 13 | /* Strictness */ 14 | "strict": true, 15 | "noUncheckedIndexedAccess": true, 16 | "checkJs": true, 17 | 18 | /* Bundled projects */ 19 | "lib": ["dom", "dom.iterable", "ES2022"], 20 | "noEmit": true, 21 | "module": "ESNext", 22 | "moduleResolution": "Bundler", 23 | "jsx": "preserve", 24 | "plugins": [{ "name": "next" }], 25 | "incremental": true, 26 | 27 | /* Path Aliases */ 28 | "baseUrl": ".", 29 | "paths": { 30 | "~/*": ["./src/*"] 31 | } 32 | }, 33 | "include": [ 34 | ".eslintrc.cjs", 35 | "next-env.d.ts", 36 | "**/*.ts", 37 | "**/*.tsx", 38 | "**/*.cjs", 39 | "**/*.js", 40 | ".next/types/**/*.ts" 41 | ], 42 | "exclude": ["node_modules"] 43 | } 44 | --------------------------------------------------------------------------------