aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md14
-rw-r--r--LICENSE.tabdeck21
-rw-r--r--NOTICE8
-rw-r--r--README.md26
-rw-r--r--config.toml53
-rw-r--r--deckit/__init__.py1
-rw-r--r--deckit/config.py40
-rw-r--r--tests/test_config.py33
8 files changed, 196 insertions, 0 deletions
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..ab4df97
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,14 @@
+# AGENTS.md
+
+Guidance for agents working in this repository.
+
+Read the design (`docs/superpowers/specs/2026-11-09-deck-it-design.md`) and the
+implementation plan (`docs/superpowers/plans/2026-11-09-deck-it.md`) first. The
+plan is the authority for interfaces and test values.
+
+- Python 3.11+, standard library only except `websocket-client` for OBS.
+- Run the suite with `python3 -m unittest discover -s tests`.
+- Each module is `deckit/modules/<name>.py` exporting `make(cfg) -> Module`; the
+ registry discovers files lazily, so a new module is a new file.
+- The server listens only on a Unix socket; `adb reverse` exposes it.
+- UDT's palette is read, never restated.
diff --git a/LICENSE.tabdeck b/LICENSE.tabdeck
new file mode 100644
index 0000000..b43e763
--- /dev/null
+++ b/LICENSE.tabdeck
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Akhilesh Tiwari
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..e494909
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,8 @@
+# NOTICE
+
+deck-it derives from tabdeck (https://github.com/silky-x0/tabdeck), which is
+distributed under the MIT License. Its notice is reproduced in
+`LICENSE.tabdeck` and must be retained in copies of the derived work.
+
+The command-center page pattern, the adb-reverse tunnel, the virtual-monitor
+setup and the stdlib HTTP server are adapted from tabdeck.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..19705e0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+# deck-it
+
+A tablet dashboard, stream-deck, and second monitor for a Slackware-current
+Hyprland desktop, themed by
+[unified-desktop-theme](https://git.danix.xyz/unified-desktop-theme/).
+
+An Android tablet shows an always-on control surface served over `adb reverse`,
+plus a virtual Hyprland monitor streamed with wayvnc and shown in AVNC. The
+surface is a module system: system stats, music, windows, OBS, LLM usage and
+build status plug in independently.
+
+Status: in development. The design is in
+`docs/superpowers/specs/`, the plan in `docs/superpowers/plans/`.
+
+## License
+
+Not decided yet. Portions derived from tabdeck are MIT; see `NOTICE` and
+`LICENSE.tabdeck`.
+
+## Development Approach
+
+This project is developed using AI-assisted tools. Code is generated with the help of AI based on human-provided specifications, design decisions, and iterative feedback.
+
+All contributions are reviewed, tested, and curated by the maintainer before being included in the codebase. AI is used as a productivity and exploration tool, while human oversight remains central to all decisions.
+
+The goal is to combine the flexibility of AI-assisted development with standard open-source practices such as transparency, review, and accountability.
diff --git a/config.toml b/config.toml
new file mode 100644
index 0000000..9800ff0
--- /dev/null
+++ b/config.toml
@@ -0,0 +1,53 @@
+# deck-it configuration.
+# Copy to ~/.config/deck-it/config.toml to customize; that file replaces this one entirely.
+
+# "auto" picks "lua" when ~/.config/hypr/hyprland.lua exists, otherwise "classic".
+hyprland = "auto"
+
+[display]
+# Resolution of the virtual monitor. "auto" reads it from the tablet over adb.
+resolution = "auto"
+# Any Hyprland monitor position: "auto-right", "auto-left", or "2646x0".
+position = "auto-right"
+scale = 1
+refresh = 60
+# Hyprland output name for the virtual monitor.
+name = "TAB"
+
+[ports]
+# Ports on the tablet's localhost, tunneled with adb reverse.
+command_center = 8765
+display = 5900
+
+[hardware]
+# Each one is "auto" or a fixed value, like net = "wlan0", battery = "BAT1",
+# cpu_sensor = "coretemp", gpu = "nvidia" | "amd" | "none".
+net = "auto"
+battery = "auto"
+cpu_sensor = "auto"
+gpu = "auto"
+
+[modules]
+# A module is disabled by setting it false. A new module file under
+# deckit/modules/ is picked up automatically; add a flag here only to turn it off.
+stats = true
+music = true
+windows = true
+obs = true
+llm = true
+sbbuilder = true
+assistant = false
+
+[obs]
+host = "127.0.0.1"
+port = 4455
+# obs-websocket password (Tools > obs-websocket Settings). Empty if auth is off.
+password = ""
+
+[llm]
+# llm-cost-dashboard's backend.
+url = "http://127.0.0.1:8420"
+
+[sbbuilder]
+# ssh host alias of the build VM; the module runs `sbb-remote jobs` there.
+host = "buildsystem"
diff --git a/deckit/__init__.py b/deckit/__init__.py
new file mode 100644
index 0000000..f5a692a
--- /dev/null
+++ b/deckit/__init__.py
@@ -0,0 +1 @@
+"""deck-it: a tablet dashboard, stream-deck and second monitor."""
diff --git a/deckit/config.py b/deckit/config.py
new file mode 100644
index 0000000..151207b
--- /dev/null
+++ b/deckit/config.py
@@ -0,0 +1,40 @@
+"""Configuration loading and validation for deck-it."""
+
+import os
+import re
+import sys
+import tomllib
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+MONITOR_NAME = re.compile(r"[A-Za-z0-9_-]{1,32}")
+
+
+def user_config_path() -> Path:
+ return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "deck-it" / "config.toml"
+
+
+def config_error(cfg: dict) -> str | None:
+ d, ports = cfg.get("display", {}), cfg.get("ports", {})
+ if not MONITOR_NAME.fullmatch(str(d.get("name", "TAB"))):
+ return "display.name must be letters, digits, - or _"
+ refresh = d.get("refresh", 60)
+ if not isinstance(refresh, int) or not 1 <= refresh <= 240:
+ return "display.refresh must be a number from 1 to 240"
+ for key in ("command_center", "display"):
+ if key in ports:
+ v = ports[key]
+ if not isinstance(v, int) or not 1024 <= v <= 65535:
+ return f"ports.{key} must be a number from 1024 to 65535"
+ if ports.get("command_center", 8765) == ports.get("display", 5900):
+ return "ports.command_center and ports.display must differ"
+ return None
+
+
+def load_config() -> dict:
+ path = user_config_path() if user_config_path().is_file() else ROOT / "config.toml"
+ with open(path, "rb") as f:
+ cfg = tomllib.load(f)
+ if err := config_error(cfg):
+ sys.exit(f"{path}: {err}")
+ return cfg
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 0000000..ff56325
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,33 @@
+# tests/test_config.py
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(ROOT))
+
+import tomllib
+import unittest
+
+import deckit.config as config
+
+
+class Config(unittest.TestCase):
+ def test_default_config_is_valid(self):
+ with open(ROOT / "config.toml", "rb") as f:
+ self.assertIsNone(config.config_error(tomllib.load(f)))
+
+ def test_rejects_bad_values(self):
+ bad = [{"display": {"name": "TAB;rm"}},
+ {"display": {"refresh": 0}},
+ {"ports": {"command_center": 80}},
+ {"ports": {"command_center": 5900, "display": 5900}}]
+ for cfg in bad:
+ self.assertIsNotNone(config.config_error(cfg), cfg)
+
+ def test_user_config_path_honours_xdg(self):
+ self.assertEqual(config.user_config_path().name, "config.toml")
+ self.assertEqual(config.user_config_path().parent.name, "deck-it")
+
+
+if __name__ == "__main__":
+ unittest.main()