https://github.com/js4jiang5/BatteryOptimizer_for_MAC
curl -s https://raw.githubusercontent.com/js4jiang5/BatteryOptimizer_for_MAC/main/setup.sh | bash
brew install swiftbarthen add this script under swiftbar.
https://github.com/js4jiang5/BatteryOptimizer_for_MAC
curl -s https://raw.githubusercontent.com/js4jiang5/BatteryOptimizer_for_MAC/main/setup.sh | bash
brew install swiftbarthen add this script under swiftbar.
| #!/usr/bin/env python3 | |
| # <xbar.title>Save your macbook battery</xbar.title> | |
| # <xbar.version>1.0</xbar.version> | |
| # <xbar.author>Neo-Neon</xbar.author> | |
| # <xbar.author.github>bitmori</xbar.author.github> | |
| # <xbar.desc>Prevent charging above 80% using the battery CLI</xbar.desc> | |
| # <xbar.image></xbar.image> | |
| # <xbar.dependencies>python3,battery</xbar.dependencies> | |
| # <swiftbar.hideAbout>true</swiftbar.hideAbout> | |
| # <swiftbar.hideRunInTerminal>true</swiftbar.hideRunInTerminal> | |
| # <swiftbar.hideLastUpdated>true</swiftbar.hideLastUpdated> | |
| # <swiftbar.hideDisablePlugin>true</swiftbar.hideDisablePlugin> | |
| # uses : https://github.com/js4jiang5/BatteryOptimizer_for_MAC | |
| import base64 | |
| import json | |
| import subprocess | |
| from dataclasses import dataclass | |
| @dataclass | |
| class BatteryStatus: | |
| percentage: int | |
| remaining: str | |
| charging: bool | |
| chargable: bool | |
| forced_discharging: bool | |
| gauge: int | |
| class Font: | |
| def __init__(self, face: str="", size: int=0): | |
| self.face = face | |
| self.size = size | |
| def blit(self) -> str: | |
| res = "" | |
| if self.face: | |
| res += f"font={self.face} " | |
| if self.size: | |
| res += f"size={self.size} " | |
| return res | |
| class Symbol: | |
| def __init__(self, name: str, **kwargs): | |
| self.name = name | |
| self.style = kwargs | |
| def blit(self) -> str: | |
| prefix = "" | |
| render_conf_b64 = "" | |
| if self.style: | |
| prefix = "sfconfig=" | |
| render_conf_b64 = base64.b64encode( | |
| json.dumps(self.style).encode()).decode() | |
| return f"sfimage={self.name} {prefix}{render_conf_b64}" | |
| class MenuItem: | |
| def __init__(self, text: str, bar: str = '|', command: list[str] = None, sym: Symbol = None, checked: bool = False, font: Font = None): | |
| self.text = text | |
| self.command = command | |
| self.sym = sym.blit() if sym else "" | |
| self.subitems: list[MenuItem] = [] | |
| self.checked = checked | |
| self.bar = bar | |
| self.font = font | |
| def blit(self, dest: list[str] = None, level: int = 0): | |
| cmd = "" | |
| font_conf = "" | |
| if self.command: | |
| shell = [f'shell={self.command[0]}'] | |
| shell.extend([f'param{i}="{param}"' for i, | |
| param in enumerate(self.command[1:])]) | |
| cmd = ' '.join(shell) | |
| if self.font and self.bar and self.text: | |
| font_conf = self.font.blit() | |
| item = f'{"--"*level}{self.text} {self.bar} {cmd} {font_conf} {self.sym} {"checked=true" if self.checked else ""}'.strip() | |
| if dest: | |
| dest.append(item) | |
| else: | |
| print(item) | |
| for sub in self.subitems: | |
| sub.blit(dest, level+1) | |
| def add(self, item: 'MenuItem'): | |
| self.subitems.append(item) | |
| def fetch_battery_status() -> BatteryStatus: | |
| csv = subprocess.check_output(["battery", "status_csv"]).decode() | |
| percentage, remaining, chargable, forced_discharging, gauge = csv.split(',') | |
| if remaining[0] == 'a': | |
| remaining = 'attached' | |
| elif ':' not in remaining: | |
| remaining = 'no estimate' | |
| return BatteryStatus(int(percentage), remaining, False, chargable[0] != 'd', forced_discharging[0] != 'n', int(gauge)) | |
| def fetch_battery_status_detail() -> list: | |
| return subprocess.check_output(["battery", "status"]).decode().strip().splitlines() | |
| def set_menubar_item(stat: BatteryStatus) -> MenuItem: | |
| if stat.charging: | |
| name = "battery.100percent.bolt" | |
| elif stat.remaining[0] == 'a': | |
| # attached | |
| name = "powerplug.portrait.fill" | |
| else: | |
| if stat.percentage >= 95: | |
| # full | |
| return MenuItem(str(stat.percentage), sym=Symbol("battery.100percent", renderingMode="Palette", colors=["green", "white"]), font=Font(size=9)) | |
| if stat.percentage >= 75: | |
| return MenuItem(str(stat.percentage), sym=Symbol("battery.75percent", renderingMode="Palette", colors=["green", "white"]), font=Font(size=9)) | |
| if stat.percentage >= 50: | |
| return MenuItem(str(stat.percentage), sym=Symbol("battery.50percent", renderingMode="Palette", colors=["green", "white"]), font=Font(size=9)) | |
| if stat.percentage >= 25: | |
| return MenuItem(str(stat.percentage), sym=Symbol("battery.25percent", renderingMode="Palette", colors=["green", "white"]), font=Font(size=9)) | |
| if stat.percentage >= 0: | |
| return MenuItem(str(stat.percentage), sym=Symbol("battery.0percent")) | |
| return MenuItem("", sym=Symbol(name)) | |
| def main(): | |
| stat = fetch_battery_status() | |
| details = fetch_battery_status_detail() | |
| try: | |
| is_gauge_enabled = "not running" not in details[2] | |
| except: | |
| is_gauge_enabled = False | |
| try: | |
| stat.charging = details[0].split(', ')[-1][0] == 'c' | |
| except: | |
| stat.charging = False | |
| section_line = MenuItem('---', bar=' ') | |
| menu_items = [set_menubar_item(stat), section_line] | |
| it_enable_gauge = MenuItem(f"Enable {stat.gauge}% battery limit", command=[ | |
| "/usr/local/bin/battery", "maintain", "recover"], checked=is_gauge_enabled) | |
| menu_items.append(it_enable_gauge) | |
| it_disable_gauge = MenuItem(f"Disable {stat.gauge}% battery limit", command=[ | |
| "/usr/local/bin/battery", "maintain", "stop"], checked=not is_gauge_enabled) | |
| menu_items.append(it_disable_gauge) | |
| menu_items.append(section_line) | |
| if stat.charging: | |
| battery_state_line = f"Battery: {stat.percentage}% (Charging)" | |
| elif stat.remaining[0] == 'a': | |
| battery_state_line = f"Battery: {stat.percentage}% (Powercord)" | |
| else: | |
| battery_state_line = f"Battery: {stat.percentage}% ({stat.remaining} remaining)" | |
| it_info_line1 = MenuItem(battery_state_line) | |
| menu_items.append(it_info_line1) | |
| if stat.forced_discharging: | |
| daemon_state_line = f"Power: forcing discharge to {stat.gauge}%" | |
| else: | |
| daemon_state_line = f'Power: smc charging {"enabled" if stat.chargable else "disabled"}' | |
| it_info_line2 = MenuItem(daemon_state_line) | |
| menu_items.append(it_info_line2) | |
| menu_items.append(section_line) | |
| details_submenu = MenuItem("Detailed info") | |
| for line in details: | |
| details_submenu.add(MenuItem(line)) | |
| menu_items.append(details_submenu) | |
| for it in menu_items: | |
| it.blit() | |
| if __name__ == "__main__": | |
| main() |