{"id":50483287,"url":"https://github.com/prostopasta/linux-privacy-switch","last_synced_at":"2026-06-01T19:30:46.910Z","repository":{"id":358011439,"uuid":"1239484733","full_name":"prostopasta/linux-privacy-switch","owner":"prostopasta","description":"Linux daemon for hardware camera+microphone privacy switch on Lenovo Legion and IdeaPad laptops. System tray indicator, ALSA/PipeWire mute, multi-device support.","archived":false,"fork":false,"pushed_at":"2026-05-15T07:29:01.000Z","size":40,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-15T09:34:52.551Z","etag":null,"topics":["camera","gnome","lenovo","linux","microphone","privacy","systemd","tray"],"latest_commit_sha":null,"homepage":"https://github.com/prostopasta/linux-privacy-switch","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/prostopasta.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-15T06:19:34.000Z","updated_at":"2026-05-15T07:29:05.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/prostopasta/linux-privacy-switch","commit_stats":null,"previous_names":["prostopasta/linux-privacy-switch"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/prostopasta/linux-privacy-switch","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prostopasta%2Flinux-privacy-switch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prostopasta%2Flinux-privacy-switch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prostopasta%2Flinux-privacy-switch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prostopasta%2Flinux-privacy-switch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/prostopasta","download_url":"https://codeload.github.com/prostopasta/linux-privacy-switch/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/prostopasta%2Flinux-privacy-switch/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33790668,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-01T02:00:06.963Z","response_time":115,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["camera","gnome","lenovo","linux","microphone","privacy","systemd","tray"],"created_at":"2026-06-01T19:30:46.315Z","updated_at":"2026-06-01T19:30:46.905Z","avatar_url":"https://github.com/prostopasta.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/banner.svg\" alt=\"linux-privacy-switch\" width=\"880\"/\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/prostopasta/linux-privacy-switch/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-MIT-blue.svg\" alt=\"MIT License\"/\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Ubuntu-22.04%20%7C%2024.04-orange.svg\" alt=\"Ubuntu\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Python-3.10%2B-blue.svg\" alt=\"Python\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/systemd-service-lightgrey.svg\" alt=\"systemd\"/\u003e\n\u003c/p\u003e\n\n---\n\n**linux-privacy-switch** makes the hardware privacy slider on Lenovo Legion / IdeaPad laptops actually work on Linux — muting the microphone and updating a tray indicator in sync with the camera.\n\n**The problem:** The ITE Embedded Controller fires a key event when the slider moves, but Linux ignores it. The camera is physically cut by the EC, but the microphone stays live and no indicator updates.\n\n**The fix:** A systemd daemon intercepts the `/dev/input/` event, detects the true camera state via V4L2 frame capture, and applies mic mute/unmute + tray icon in sync.\n\n---\n\n## Supported devices\n\n| Device | DMI `product_name` | Status |\n|---|---|---|\n| Lenovo Legion 5 15IRX10 | 83LY | ✅ Tested |\n| Lenovo Legion 5 Gen 6 (AMD) | 82JU | 🔧 Needs testing |\n| Lenovo IdeaPad 5 | IdeaPad 5 | 🔧 Needs testing |\n\nAdding a new device takes [~5 minutes](#adding-a-new-device).\n\n---\n\n## Quick install\n\n```bash\ngit clone https://github.com/prostopasta/linux-privacy-switch.git\ncd linux-privacy-switch\nsudo bash install.sh\n```\n\nDetects your device by DMI, installs both systemd services, starts the tray.\n\n```bash\nbash tests/verify.sh   # post-install check\n```\n\n---\n\n## How it works\n\n```\nPhysical slider → ITE EC → /dev/input/eventN\n    → privacy-switch daemon  (root, system service)\n        ├── V4L2 frame capture → detect true camera state\n        ├── amixer sset Capture cap/nocap         (ALSA)\n        ├── wpctl set-mute @DEFAULT_AUDIO_SOURCE@  (PipeWire)\n        ├── notify-send\n        └── heartbeat file  every 10 s\n    → privacy-tray  (user service, after graphical-session.target)\n        ├── tray icon  ← polls state file every 800 ms\n        └── wpctl set-volume 50%  on enable\n```\n\n### Startup sequence\n\nOn every start the daemon:\n\n1. **Waits** until system uptime ≥ 15 s — gives the ITE EC time to apply the switch position to the camera sensor before probing.\n2. **Determines initial state:**\n   - *Hardware kill devices* (`has_hw_camera_kill = true`): captures one V4L2 frame. A black frame (\u003c `nonzero_threshold` % nonzero bytes, default 3%) means the slider is **OFF**; a live frame means **ON**. This is necessary because `camera_power` sysfs always reads `0` on affected hardware — the EC controls the sensor directly.\n   - *Software-only devices* (`has_hw_camera_kill = false`): the sensor is always powered, so probing is skipped. The daemon reads the last saved state file.\n3. **Drains** any buffered EC init-events so they aren't misread as user toggles.\n4. **Enters the event loop.** From here, every EC key event is a real slider movement.\n\n### Periodic health check\n\nEvery **60 seconds** the daemon re-probes the camera in a background thread. If the result disagrees with saved state (e.g. after a crash-and-restart), it self-corrects automatically.\n\n### Daemon liveness\n\nThe daemon writes a Unix timestamp to `/var/lib/linux-privacy-switch/heartbeat` every 10 s. The tray reads it: if the file is older than 30 s the icon switches to **⚠ Daemon not responding** until the service recovers (`Restart=always` in the unit file handles this automatically).\n\n---\n\n## Device modes\n\nThe daemon operates in one of two modes depending on the device profile:\n\n| Mode | `has_hw_camera_kill` | V4L2 probe | State source at boot |\n|---|---|---|---|\n| Hardware kill | `true` | Yes — black frame = OFF | V4L2 reading (authoritative) |\n| Software-only | `false` | No | Saved state file |\n\n**Hardware kill** (e.g. Lenovo Legion slider): the ITE Embedded Controller physically disconnects the camera sensor from the bus. A V4L2 frame captured while the slider is OFF contains almost no signal (~1% nonzero bytes), making the probe reliable. The daemon runs it once at startup and repeats every 60 s.\n\n**Software-only** (e.g. Fn+key on IdeaPad 5): the sensor remains powered regardless of the key state. Probing would return a live frame even when \"disabled\", so the daemon skips V4L2 entirely and relies on the saved state file.\n\nSet `has_hw_camera_kill` appropriately in your device profile. The `install.sh` installer asks this as an interactive question.\n\n---\n\n## Tray indicator\n\n| Icon | Meaning |\n|---|---|\n| 🟢 Green | Camera and microphone **enabled** |\n| 🔴 Red | Camera and microphone **muted** |\n| ⏳ Starting | Daemon is running the startup probe (~15–18 s) |\n| ⚠️ Not responding | Daemon crashed; systemd auto-restart pending |\n\nThe tray is **display-only** — toggle with the physical slider only. Right-click → Quit.\n\nLaunched automatically at login via `linux-privacy-tray.service` (systemd user service).\n\n---\n\n## Service management\n\n```bash\n# Status\nsudo systemctl status linux-privacy-switch\nsystemctl --user status linux-privacy-tray\n\n# Restart both services at once (waits for startup probe to finish)\nsudo privacy-restart\n\n# Live logs\nsudo journalctl -u linux-privacy-switch -f\n```\n\n---\n\n## Adding a new device\n\n```bash\n# 1. Find DMI strings and input device name\nsudo python3 src/privacy-switch.py --detect --config config/devices.conf\n\n# 2. Find the slider key code\nsudo python3 src/privacy-switch.py --monitor\n# Move the slider → you'll see  device_name  and  code=XXX\n```\n\nAdd a section to `config/devices.conf`:\n\n```ini\n[my-laptop-model]\nmatch_sys_vendor       = LENOVO\nmatch_product_name     = \u003cproduct_name from --detect\u003e\ninput_device_name      = \u003cdevice name from --detect\u003e\nkey_code               = \u003ccode from --monitor\u003e\nalsa_card              = 0\nalsa_control           = Capture\nhas_hw_camera_kill     = false   # see Device modes section\nsync_camera            = true\nsync_mic               = true\ndescription            = Short description\n```\n\nReinstall to apply:\n\n```bash\nsudo bash install.sh\n```\n\n### Scenario A — Physical slider (hardware kill, e.g. Legion 5)\n\n1. Run `--detect` to get DMI strings and input device name\n2. Run `--monitor` to identify the slider key code\n3. Add section to `devices.conf` with `has_hw_camera_kill = true`\n4. Reinstall: `sudo bash install.sh`\n5. Run `--calibrate` to find the optimal brightness threshold (see below)\n6. Test: move slider → camera cuts out, mic mutes, tray icon updates\n\n### Scenario B — Software-only Fn key (e.g. IdeaPad 5)\n\n1. Run `--detect` / `--monitor` as above\n2. Add section with `has_hw_camera_kill = false`\n3. Reinstall: `sudo bash install.sh`\n4. The daemon tracks state in software only — no V4L2 probe is performed\n5. Test: press key → mic mutes/unmutes, tray updates; camera indicator changes but the sensor stays live (this is expected)\n\n### Scenario C — No dedicated key\n\nNot yet supported. Would require integrating a hotkey daemon (e.g. xbindkeys). Contributions welcome.\n\nPull requests with new device profiles are welcome.\n\n---\n\n## Calibration\n\nOnly relevant for devices with `has_hw_camera_kill = true`.\n\nThe daemon detects camera state by comparing V4L2 frame brightness against a threshold. The default is **3%** nonzero bytes — a safe midpoint between a typical OFF frame (~1.2%) and a typical ON frame (~5–6%).\n\n**When to calibrate:** when adding a new hardware-kill device, or if the indicator flips incorrectly.\n\n**Important:** If your model has no physical lens cover, cover the camera with your finger during the OFF-state measurement — some sensors output a faint residual signal even when the EC cuts power.\n\n```bash\nsudo python3 /usr/local/bin/privacy-switch --calibrate\n```\n\nThe wizard measures brightness in both states, suggests the optimal threshold, and offers to save `nonzero_threshold` to your device profile. The installer also offers to run calibration on first install.\n\n---\n\n## Troubleshooting\n\n**Slider direction is inverted** — toggle once; the daemon self-corrects and stays in sync from then on.\n\n**Device not detected** — run `--detect`, add a profile to `devices.conf`.\n\n**Tray not starting** — `systemctl --user restart linux-privacy-tray`.\n\n**Wrong state after reboot** — the daemon needs ~15–18 s to finish the startup probe. Wait a moment, then check `cat /var/lib/linux-privacy-switch/state`.\n\n**Mic not muting during a video call** — while another app is streaming the camera, the V4L2 probe returns `EBUSY` and is skipped; the daemon uses the last saved state. Toggle the slider once to force a sync.\n\n---\n\n## Future extensions\n\nThe hardware privacy switch concept is not limited to camera and microphone. The same daemon architecture can be extended to cut any signal source on toggle:\n\n- **Wi-Fi** — `rfkill block wifi`\n- **Bluetooth** — `rfkill block bluetooth`\n- **USB ports** — cut power via `uhubctl` or hub-level sysfs\n- **Ethernet** — `ip link set eth0 down`\n\nEach source would get its own `sync_*` flag in the device profile, making the slider a universal hardware kill switch for all radio and I/O interfaces. Pull requests adding new sync targets are welcome.\n\n---\n\n## Related projects\n\n- [johnfanv2/LenovoLegionLinux](https://github.com/johnfanv2/LenovoLegionLinux) — fans, power, RGB for Lenovo Legion on Linux\n\n---\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprostopasta%2Flinux-privacy-switch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fprostopasta%2Flinux-privacy-switch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fprostopasta%2Flinux-privacy-switch/lists"}