Skip to content

Instantly share code, notes, and snippets.

@bitmori
Forked from ioExpander/batterySaver.1m.sh
Last active March 16, 2025 20:11
Show Gist options
  • Select an option

  • Save bitmori/b28988fc7fcfef3b1c87c8c293c38f19 to your computer and use it in GitHub Desktop.

Select an option

Save bitmori/b28988fc7fcfef3b1c87c8c293c38f19 to your computer and use it in GitHub Desktop.
SwiftBar plugin to save your ARM based macbook's battery by limiting charging to 80% using the battery CLI.
#!/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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment