ds4color 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ds4color-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abhyudit Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: ds4color
3
+ Version: 1.0.0
4
+ Summary: Set the DualShock 4 lightbar to a solid colour on macOS over USB or Bluetooth
5
+ Author-email: Abhyudit Singh <abhyudit.singh@research.iiit.ac.in>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/bitmap4/ds4-lightbar-macos
8
+ Project-URL: Issues, https://github.com/bitmap4/ds4-lightbar-macos/issues
9
+ Keywords: dualshock4,ds4,playstation,controller,lightbar,macos,hid
10
+ Classifier: Environment :: MacOS X
11
+ Classifier: Operating System :: MacOS :: MacOS X
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Games/Entertainment
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: hidapi>=0.14
18
+ Dynamic: license-file
19
+
20
+ # ds4-lightbar-macos
21
+
22
+ A DualShock 4 connected to a Mac over Bluetooth never gets told what colour to
23
+ make its lightbar, so it just pulses white forever even though it works fine.
24
+ On a PS4 the console sends that command; macOS doesn't. This is a tiny tool
25
+ that sends it, so the light goes solid instead.
26
+
27
+ It also sets the colour automatically every time the controller connects, over
28
+ USB or Bluetooth, and only after it has read a real input report from the pad.
29
+ That last part matters: if the light goes solid, the controller is genuinely
30
+ connected and sending input. If it stays blinking white, something is wrong
31
+ with the connection and you know not to trust it.
32
+
33
+ ## Install
34
+
35
+ Homebrew:
36
+
37
+ ```bash
38
+ brew install bitmap4/tap/ds4color
39
+ ds4color enable
40
+ ```
41
+
42
+ Or with pipx:
43
+
44
+ ```bash
45
+ pipx install ds4color
46
+ ds4color enable
47
+ ```
48
+
49
+ `ds4color enable` starts a small background agent that runs at login and sets
50
+ the colour whenever the controller connects. `ds4color disable` stops it.
51
+
52
+ ## Usage
53
+
54
+ ```bash
55
+ ds4color # current setting and whether the pad is connected
56
+ ds4color blue # pick a colour
57
+ ds4color blue 20 # colour at 20% brightness
58
+ ds4color 20 # just the brightness
59
+ ds4color 255 90 0 # custom rgb
60
+ ds4color night # dim warm preset
61
+ ds4color day # bright blue preset
62
+ ds4color list # named colours
63
+ ds4color presets # saved presets
64
+ ds4color save night # save the current setting as a preset
65
+ ```
66
+
67
+ Changes apply right away if the pad is connected, and the agent reuses them on
68
+ every reconnect.
69
+
70
+ If you own more than one DS4 and want this to touch only one of them, connect
71
+ that one and run `ds4color bind`. `ds4color unbind` goes back to matching any
72
+ DualShock 4.
73
+
74
+ ## Config
75
+
76
+ `~/.config/ds4color/config.json`:
77
+
78
+ ```json
79
+ {
80
+ "color": "blue",
81
+ "brightness": 100,
82
+ "controller_serial": null,
83
+ "presets": {
84
+ "day": { "color": "blue", "brightness": 100 },
85
+ "night": { "color": "warm", "brightness": 12 }
86
+ }
87
+ }
88
+ ```
89
+
90
+ `controller_serial` is the controller's Bluetooth MAC (the DS4 reports the same
91
+ value over USB). Leave it `null` to match any DS4.
92
+
93
+ ## How it works
94
+
95
+ Setting the lightbar is a HID output report. Over USB it's report `0x05`; over
96
+ Bluetooth it's report `0x11` with a CRC32 the controller checks before it
97
+ listens. The tool figures out which transport you're on by looking at the input
98
+ report the pad sends back, then writes the matching one. That's the whole trick.
99
+
100
+ ## Uninstall
101
+
102
+ ```bash
103
+ ds4color disable
104
+ brew uninstall ds4color # or: pipx uninstall ds4color
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,90 @@
1
+ # ds4-lightbar-macos
2
+
3
+ A DualShock 4 connected to a Mac over Bluetooth never gets told what colour to
4
+ make its lightbar, so it just pulses white forever even though it works fine.
5
+ On a PS4 the console sends that command; macOS doesn't. This is a tiny tool
6
+ that sends it, so the light goes solid instead.
7
+
8
+ It also sets the colour automatically every time the controller connects, over
9
+ USB or Bluetooth, and only after it has read a real input report from the pad.
10
+ That last part matters: if the light goes solid, the controller is genuinely
11
+ connected and sending input. If it stays blinking white, something is wrong
12
+ with the connection and you know not to trust it.
13
+
14
+ ## Install
15
+
16
+ Homebrew:
17
+
18
+ ```bash
19
+ brew install bitmap4/tap/ds4color
20
+ ds4color enable
21
+ ```
22
+
23
+ Or with pipx:
24
+
25
+ ```bash
26
+ pipx install ds4color
27
+ ds4color enable
28
+ ```
29
+
30
+ `ds4color enable` starts a small background agent that runs at login and sets
31
+ the colour whenever the controller connects. `ds4color disable` stops it.
32
+
33
+ ## Usage
34
+
35
+ ```bash
36
+ ds4color # current setting and whether the pad is connected
37
+ ds4color blue # pick a colour
38
+ ds4color blue 20 # colour at 20% brightness
39
+ ds4color 20 # just the brightness
40
+ ds4color 255 90 0 # custom rgb
41
+ ds4color night # dim warm preset
42
+ ds4color day # bright blue preset
43
+ ds4color list # named colours
44
+ ds4color presets # saved presets
45
+ ds4color save night # save the current setting as a preset
46
+ ```
47
+
48
+ Changes apply right away if the pad is connected, and the agent reuses them on
49
+ every reconnect.
50
+
51
+ If you own more than one DS4 and want this to touch only one of them, connect
52
+ that one and run `ds4color bind`. `ds4color unbind` goes back to matching any
53
+ DualShock 4.
54
+
55
+ ## Config
56
+
57
+ `~/.config/ds4color/config.json`:
58
+
59
+ ```json
60
+ {
61
+ "color": "blue",
62
+ "brightness": 100,
63
+ "controller_serial": null,
64
+ "presets": {
65
+ "day": { "color": "blue", "brightness": 100 },
66
+ "night": { "color": "warm", "brightness": 12 }
67
+ }
68
+ }
69
+ ```
70
+
71
+ `controller_serial` is the controller's Bluetooth MAC (the DS4 reports the same
72
+ value over USB). Leave it `null` to match any DS4.
73
+
74
+ ## How it works
75
+
76
+ Setting the lightbar is a HID output report. Over USB it's report `0x05`; over
77
+ Bluetooth it's report `0x11` with a CRC32 the controller checks before it
78
+ listens. The tool figures out which transport you're on by looking at the input
79
+ report the pad sends back, then writes the matching one. That's the whole trick.
80
+
81
+ ## Uninstall
82
+
83
+ ```bash
84
+ ds4color disable
85
+ brew uninstall ds4color # or: pipx uninstall ds4color
86
+ ```
87
+
88
+ ## License
89
+
90
+ MIT
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,67 @@
1
+ import os
2
+ import sys
3
+ import subprocess
4
+
5
+ LABEL = "com.ds4color.agent"
6
+ PLIST = os.path.join(os.path.expanduser("~"), "Library", "LaunchAgents", LABEL + ".plist")
7
+ LOG = os.path.join(os.path.expanduser("~"), ".config", "ds4color", "watch.log")
8
+
9
+ PLIST_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
10
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
11
+ <plist version="1.0">
12
+ <dict>
13
+ <key>Label</key>
14
+ <string>{label}</string>
15
+ <key>ProgramArguments</key>
16
+ <array>
17
+ <string>{python}</string>
18
+ <string>-m</string>
19
+ <string>ds4color.watch</string>
20
+ </array>
21
+ <key>RunAtLoad</key>
22
+ <true/>
23
+ <key>KeepAlive</key>
24
+ <true/>
25
+ <key>ThrottleInterval</key>
26
+ <integer>5</integer>
27
+ <key>StandardOutPath</key>
28
+ <string>{log}</string>
29
+ <key>StandardErrorPath</key>
30
+ <string>{log}</string>
31
+ </dict>
32
+ </plist>
33
+ """
34
+
35
+
36
+ def _domain():
37
+ return "gui/%d" % os.getuid()
38
+
39
+
40
+ def is_running():
41
+ r = subprocess.run(["launchctl", "print", "%s/%s" % (_domain(), LABEL)],
42
+ capture_output=True)
43
+ return r.returncode == 0
44
+
45
+
46
+ def enable():
47
+ os.makedirs(os.path.dirname(PLIST), exist_ok=True)
48
+ os.makedirs(os.path.dirname(LOG), exist_ok=True)
49
+ with open(PLIST, "w") as f:
50
+ f.write(PLIST_TEMPLATE.format(label=LABEL, python=sys.executable, log=LOG))
51
+ subprocess.run(["launchctl", "bootout", "%s/%s" % (_domain(), LABEL)], capture_output=True)
52
+ r = subprocess.run(["launchctl", "bootstrap", _domain(), PLIST], capture_output=True, text=True)
53
+ if r.returncode != 0:
54
+ print(r.stderr.strip() or "failed to start the agent")
55
+ return 1
56
+ print("agent enabled, runs at login")
57
+ return 0
58
+
59
+
60
+ def disable():
61
+ subprocess.run(["launchctl", "bootout", "%s/%s" % (_domain(), LABEL)], capture_output=True)
62
+ try:
63
+ os.remove(PLIST)
64
+ except FileNotFoundError:
65
+ pass
66
+ print("agent disabled")
67
+ return 0
@@ -0,0 +1,120 @@
1
+ import sys
2
+
3
+ from . import core
4
+ from . import agent
5
+
6
+ USAGE = """ds4color - set the DS4 lightbar color
7
+
8
+ ds4color show current setting and status
9
+ ds4color list list named colors
10
+ ds4color blue set a color
11
+ ds4color blue 20 set color and brightness (percent)
12
+ ds4color 20 set brightness only
13
+ ds4color 255 90 0 custom rgb
14
+ ds4color 255 90 0 40 custom rgb and brightness
15
+ ds4color night load the night preset
16
+ ds4color day load the day preset
17
+ ds4color presets list presets
18
+ ds4color save night save the current setting as a preset
19
+ ds4color bind lock to the controller connected right now
20
+ ds4color unbind allow any DualShock 4
21
+ ds4color enable start the background agent (sets color on connect)
22
+ ds4color disable stop the background agent
23
+ """
24
+
25
+
26
+ def status():
27
+ cfg = core.load_config()
28
+ bound = cfg.get("controller_serial")
29
+ print("color:", cfg["color"])
30
+ print("brightness:", str(cfg["brightness"]) + "%")
31
+ print("rgb:", core.resolve_rgb(cfg["color"], cfg["brightness"]))
32
+ print("bound to:", bound if bound else "any DualShock 4")
33
+ print("controller:", "connected" if core.find_target(bound) else "not connected")
34
+ print("agent:", "running" if agent.is_running() else "stopped")
35
+
36
+
37
+ def is_int(s):
38
+ try:
39
+ int(s)
40
+ return True
41
+ except ValueError:
42
+ return False
43
+
44
+
45
+ def main():
46
+ args = sys.argv[1:]
47
+ cfg = core.load_config()
48
+
49
+ if not args:
50
+ status()
51
+ return 0
52
+
53
+ cmd = args[0].lower()
54
+ if cmd in ("list", "colors"):
55
+ print(", ".join(sorted(core.PALETTE)))
56
+ return 0
57
+ if cmd in ("status", "-s"):
58
+ status()
59
+ return 0
60
+ if cmd == "enable":
61
+ return agent.enable()
62
+ if cmd == "disable":
63
+ return agent.disable()
64
+ if cmd in ("presets", "preset"):
65
+ for name, p in cfg["presets"].items():
66
+ print(name, p["color"], str(p["brightness"]) + "%")
67
+ return 0
68
+ if cmd == "bind":
69
+ dev = core.find_target(None)
70
+ if not dev:
71
+ print("no controller connected")
72
+ return 3
73
+ cfg["controller_serial"] = dev.get("serial_number") or ""
74
+ core.save_config(cfg)
75
+ print("bound to", cfg["controller_serial"] or "(empty serial)")
76
+ return 0
77
+ if cmd == "unbind":
78
+ cfg["controller_serial"] = None
79
+ core.save_config(cfg)
80
+ print("unbound")
81
+ return 0
82
+ if cmd == "save" and len(args) >= 2:
83
+ name = args[1].lower()
84
+ cfg["presets"][name] = {"color": cfg["color"], "brightness": cfg["brightness"]}
85
+ core.save_config(cfg)
86
+ print("saved preset", name)
87
+ return 0
88
+ if cmd in cfg["presets"]:
89
+ cfg.update(cfg["presets"][cmd])
90
+ core.save_config(cfg)
91
+ print(cmd, "->", core.resolve_rgb(cfg["color"], cfg["brightness"]))
92
+ print(core.apply_from_config()[1])
93
+ return 0
94
+
95
+ ints = [int(a) for a in args if is_int(a)]
96
+ names = [a for a in args if not is_int(a)]
97
+
98
+ if names and names[0].lower() in core.PALETTE:
99
+ cfg["color"] = names[0].lower()
100
+ if ints:
101
+ cfg["brightness"] = ints[0]
102
+ elif len(ints) >= 3:
103
+ cfg["color"] = ints[:3]
104
+ if len(ints) >= 4:
105
+ cfg["brightness"] = ints[3]
106
+ elif len(ints) == 1:
107
+ cfg["brightness"] = ints[0]
108
+ else:
109
+ print(USAGE)
110
+ return 2
111
+
112
+ cfg["brightness"] = max(0, min(100, int(cfg["brightness"])))
113
+ core.save_config(cfg)
114
+ print("rgb:", core.resolve_rgb(cfg["color"], cfg["brightness"]))
115
+ print(core.apply_from_config()[1])
116
+ return 0
117
+
118
+
119
+ if __name__ == "__main__":
120
+ sys.exit(main())
@@ -0,0 +1,130 @@
1
+ import os
2
+ import json
3
+ import time
4
+ import zlib
5
+
6
+ import hid
7
+
8
+ VENDOR_ID = 0x054C
9
+ PRODUCT_IDS = {0x05C4, 0x09CC} # DS4 v1 and v2
10
+
11
+ CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".config", "ds4color")
12
+ CONFIG_PATH = os.path.join(CONFIG_DIR, "config.json")
13
+
14
+ PALETTE = {
15
+ "blue": (0, 0, 255),
16
+ "red": (255, 0, 0),
17
+ "green": (0, 255, 0),
18
+ "cyan": (0, 255, 255),
19
+ "magenta": (255, 0, 255),
20
+ "pink": (255, 40, 120),
21
+ "purple": (140, 0, 255),
22
+ "orange": (255, 90, 0),
23
+ "yellow": (255, 200, 0),
24
+ "white": (255, 255, 255),
25
+ "warm": (255, 120, 30),
26
+ }
27
+
28
+
29
+ def load_config():
30
+ try:
31
+ with open(CONFIG_PATH) as f:
32
+ cfg = json.load(f)
33
+ except Exception:
34
+ cfg = {}
35
+ cfg.setdefault("color", "blue")
36
+ cfg.setdefault("brightness", 100)
37
+ cfg.setdefault("controller_serial", None)
38
+ presets = cfg.setdefault("presets", {})
39
+ presets.setdefault("day", {"color": "blue", "brightness": 100})
40
+ presets.setdefault("night", {"color": "warm", "brightness": 12})
41
+ return cfg
42
+
43
+
44
+ def save_config(cfg):
45
+ os.makedirs(CONFIG_DIR, exist_ok=True)
46
+ with open(CONFIG_PATH, "w") as f:
47
+ json.dump(cfg, f, indent=2)
48
+ f.write("\n")
49
+
50
+
51
+ def resolve_rgb(color, brightness):
52
+ base = PALETTE.get(color.lower(), PALETTE["blue"]) if isinstance(color, str) else tuple(int(c) for c in color)
53
+ scale = max(0, min(100, int(brightness))) / 100.0
54
+ return tuple(max(0, min(255, round(c * scale))) for c in base)
55
+
56
+
57
+ def find_target(serial=None):
58
+ want = (serial or "").upper() or None
59
+ for d in hid.enumerate():
60
+ if d.get("vendor_id") == VENDOR_ID and d.get("product_id") in PRODUCT_IDS:
61
+ ser = (d.get("serial_number") or "").upper()
62
+ if want is None or ser == want:
63
+ return d
64
+ return None
65
+
66
+
67
+ def _usb_report(r, g, b):
68
+ buf = bytearray(32)
69
+ buf[0] = 0x05
70
+ buf[1] = 0xFF
71
+ buf[6], buf[7], buf[8] = r, g, b
72
+ return bytes(buf)
73
+
74
+
75
+ def _bt_report(r, g, b):
76
+ buf = bytearray(78)
77
+ buf[0] = 0x11
78
+ buf[1] = 0xC0
79
+ buf[3] = 0x07
80
+ buf[8], buf[9], buf[10] = r, g, b
81
+ # DS4 checks a CRC32 over a 0xA2 prefix plus the report, or it ignores us
82
+ crc = zlib.crc32(bytes([0xA2]) + bytes(buf[0:74])) & 0xFFFFFFFF
83
+ buf[74:78] = crc.to_bytes(4, "little")
84
+ return bytes(buf)
85
+
86
+
87
+ def _read_input(h, attempts=12, timeout_ms=200):
88
+ for _ in range(attempts):
89
+ try:
90
+ data = h.read(128, timeout_ms)
91
+ except TypeError:
92
+ h.set_nonblocking(False)
93
+ data = h.read(128)
94
+ if data and len(data) >= 10:
95
+ return data[0], len(data)
96
+ time.sleep(0.02)
97
+ return None
98
+
99
+
100
+ def apply(rgb, serial=None):
101
+ d = find_target(serial)
102
+ if not d:
103
+ return 3, "controller not found"
104
+
105
+ h = hid.device()
106
+ try:
107
+ try:
108
+ h.open_path(d["path"])
109
+ except Exception:
110
+ h.open(VENDOR_ID, d["product_id"])
111
+ except Exception as e:
112
+ return 5, "could not open device: %s" % e
113
+
114
+ try:
115
+ rid = _read_input(h)
116
+ if rid is None:
117
+ return 4, "connected but no input yet, leaving the light alone"
118
+ report_id, length = rid
119
+ over_bt = report_id == 0x11 or length >= 70
120
+ rep = _bt_report(*rgb) if over_bt else _usb_report(*rgb)
121
+ if h.write(rep) <= 0:
122
+ return 6, "input works but writing the color failed"
123
+ return 0, "%s ok, set RGB%s" % ("bluetooth" if over_bt else "usb", rgb)
124
+ finally:
125
+ h.close()
126
+
127
+
128
+ def apply_from_config():
129
+ cfg = load_config()
130
+ return apply(resolve_rgb(cfg["color"], cfg["brightness"]), cfg.get("controller_serial"))
@@ -0,0 +1,50 @@
1
+ import sys
2
+ import time
3
+ from datetime import datetime
4
+
5
+ from . import core
6
+
7
+ POLL = 1.5
8
+ RETRY_WINDOW = 20
9
+
10
+
11
+ def log(msg):
12
+ print("[%s] %s" % (datetime.now().strftime("%H:%M:%S"), msg), flush=True)
13
+
14
+
15
+ def main():
16
+ log("watching")
17
+ present = False
18
+ applied = False
19
+ deadline = 0.0
20
+ while True:
21
+ cfg = core.load_config()
22
+ here = core.find_target(cfg.get("controller_serial")) is not None
23
+
24
+ if here and not present:
25
+ present, applied = True, False
26
+ deadline = time.time() + RETRY_WINDOW
27
+ elif not here and present:
28
+ present, applied = False, False
29
+ log("disconnected")
30
+
31
+ if present and not applied:
32
+ code, msg = core.apply_from_config()
33
+ if code == 0:
34
+ log(msg)
35
+ applied = True
36
+ elif time.time() > deadline:
37
+ log("gave up: " + msg)
38
+ applied = True
39
+
40
+ time.sleep(POLL)
41
+
42
+
43
+ if __name__ == "__main__":
44
+ try:
45
+ main()
46
+ except KeyboardInterrupt:
47
+ pass
48
+ except Exception as e:
49
+ log("crashed: %s" % e)
50
+ sys.exit(1)
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: ds4color
3
+ Version: 1.0.0
4
+ Summary: Set the DualShock 4 lightbar to a solid colour on macOS over USB or Bluetooth
5
+ Author-email: Abhyudit Singh <abhyudit.singh@research.iiit.ac.in>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/bitmap4/ds4-lightbar-macos
8
+ Project-URL: Issues, https://github.com/bitmap4/ds4-lightbar-macos/issues
9
+ Keywords: dualshock4,ds4,playstation,controller,lightbar,macos,hid
10
+ Classifier: Environment :: MacOS X
11
+ Classifier: Operating System :: MacOS :: MacOS X
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Games/Entertainment
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: hidapi>=0.14
18
+ Dynamic: license-file
19
+
20
+ # ds4-lightbar-macos
21
+
22
+ A DualShock 4 connected to a Mac over Bluetooth never gets told what colour to
23
+ make its lightbar, so it just pulses white forever even though it works fine.
24
+ On a PS4 the console sends that command; macOS doesn't. This is a tiny tool
25
+ that sends it, so the light goes solid instead.
26
+
27
+ It also sets the colour automatically every time the controller connects, over
28
+ USB or Bluetooth, and only after it has read a real input report from the pad.
29
+ That last part matters: if the light goes solid, the controller is genuinely
30
+ connected and sending input. If it stays blinking white, something is wrong
31
+ with the connection and you know not to trust it.
32
+
33
+ ## Install
34
+
35
+ Homebrew:
36
+
37
+ ```bash
38
+ brew install bitmap4/tap/ds4color
39
+ ds4color enable
40
+ ```
41
+
42
+ Or with pipx:
43
+
44
+ ```bash
45
+ pipx install ds4color
46
+ ds4color enable
47
+ ```
48
+
49
+ `ds4color enable` starts a small background agent that runs at login and sets
50
+ the colour whenever the controller connects. `ds4color disable` stops it.
51
+
52
+ ## Usage
53
+
54
+ ```bash
55
+ ds4color # current setting and whether the pad is connected
56
+ ds4color blue # pick a colour
57
+ ds4color blue 20 # colour at 20% brightness
58
+ ds4color 20 # just the brightness
59
+ ds4color 255 90 0 # custom rgb
60
+ ds4color night # dim warm preset
61
+ ds4color day # bright blue preset
62
+ ds4color list # named colours
63
+ ds4color presets # saved presets
64
+ ds4color save night # save the current setting as a preset
65
+ ```
66
+
67
+ Changes apply right away if the pad is connected, and the agent reuses them on
68
+ every reconnect.
69
+
70
+ If you own more than one DS4 and want this to touch only one of them, connect
71
+ that one and run `ds4color bind`. `ds4color unbind` goes back to matching any
72
+ DualShock 4.
73
+
74
+ ## Config
75
+
76
+ `~/.config/ds4color/config.json`:
77
+
78
+ ```json
79
+ {
80
+ "color": "blue",
81
+ "brightness": 100,
82
+ "controller_serial": null,
83
+ "presets": {
84
+ "day": { "color": "blue", "brightness": 100 },
85
+ "night": { "color": "warm", "brightness": 12 }
86
+ }
87
+ }
88
+ ```
89
+
90
+ `controller_serial` is the controller's Bluetooth MAC (the DS4 reports the same
91
+ value over USB). Leave it `null` to match any DS4.
92
+
93
+ ## How it works
94
+
95
+ Setting the lightbar is a HID output report. Over USB it's report `0x05`; over
96
+ Bluetooth it's report `0x11` with a CRC32 the controller checks before it
97
+ listens. The tool figures out which transport you're on by looking at the input
98
+ report the pad sends back, then writes the matching one. That's the whole trick.
99
+
100
+ ## Uninstall
101
+
102
+ ```bash
103
+ ds4color disable
104
+ brew uninstall ds4color # or: pipx uninstall ds4color
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ ds4color/__init__.py
5
+ ds4color/agent.py
6
+ ds4color/cli.py
7
+ ds4color/core.py
8
+ ds4color/watch.py
9
+ ds4color.egg-info/PKG-INFO
10
+ ds4color.egg-info/SOURCES.txt
11
+ ds4color.egg-info/dependency_links.txt
12
+ ds4color.egg-info/entry_points.txt
13
+ ds4color.egg-info/requires.txt
14
+ ds4color.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ds4color = ds4color.cli:main
@@ -0,0 +1 @@
1
+ hidapi>=0.14
@@ -0,0 +1 @@
1
+ ds4color
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ds4color"
7
+ version = "1.0.0"
8
+ description = "Set the DualShock 4 lightbar to a solid colour on macOS over USB or Bluetooth"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{ name = "Abhyudit Singh", email = "abhyudit.singh@research.iiit.ac.in" }]
13
+ keywords = ["dualshock4", "ds4", "playstation", "controller", "lightbar", "macos", "hid"]
14
+ dependencies = ["hidapi>=0.14"]
15
+ classifiers = [
16
+ "Environment :: MacOS X",
17
+ "Operating System :: MacOS :: MacOS X",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Games/Entertainment",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/bitmap4/ds4-lightbar-macos"
24
+ Issues = "https://github.com/bitmap4/ds4-lightbar-macos/issues"
25
+
26
+ [project.scripts]
27
+ ds4color = "ds4color.cli:main"
28
+
29
+ [tool.setuptools]
30
+ packages = ["ds4color"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+