frobar-ng: Colors

This commit is contained in:
Geoffrey Frogeye 2025-01-03 18:56:20 +01:00
parent 7b54ad0578
commit 953aa13cb6
Signed by: geoffrey
GPG key ID: C72403E7F82E6AD8
2 changed files with 136 additions and 44 deletions

View file

@ -5,18 +5,24 @@ import datetime
import enum import enum
import ipaddress import ipaddress
import logging import logging
import random
import signal import signal
import socket import socket
import typing import typing
import coloredlogs
import i3ipc import i3ipc
import i3ipc.aio import i3ipc.aio
import psutil import psutil
import rich.color
import rich.logging
import rich.terminal_theme
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s") logging.basicConfig(
log = logging.getLogger() level="DEBUG",
format="%(message)s",
datefmt="[%X]",
handlers=[rich.logging.RichHandler()],
)
log = logging.getLogger("frobar")
T = typing.TypeVar("T", bound="ComposableText") T = typing.TypeVar("T", bound="ComposableText")
P = typing.TypeVar("P", bound="ComposableText") P = typing.TypeVar("P", bound="ComposableText")
@ -85,12 +91,6 @@ class ComposableText(typing.Generic[P, C]):
return self.generateMarkup() return self.generateMarkup()
def randomColor(seed: int | bytes | None = None) -> str:
if seed is not None:
random.seed(seed)
return "#" + "".join(f"{random.randint(0, 0xff):02x}" for _ in range(3))
class Button(enum.Enum): class Button(enum.Enum):
CLICK_LEFT = "1" CLICK_LEFT = "1"
CLICK_MIDDLE = "2" CLICK_MIDDLE = "2"
@ -104,11 +104,16 @@ class Section(ComposableText):
Colorable block separated by chevrons Colorable block separated by chevrons
""" """
def __init__(self, parent: "Module", sortKey: Sortable = 0) -> None: def __init__(
self,
parent: "Module",
sortKey: Sortable = 0,
color: rich.color.Color = rich.color.Color.default(),
) -> None:
super().__init__(parent=parent, sortKey=sortKey) super().__init__(parent=parent, sortKey=sortKey)
self.parent: "Module" self.parent: "Module"
self.color = color
self.color = randomColor()
self.desiredText: str | None = None self.desiredText: str | None = None
self.text = "" self.text = ""
self.targetSize = -1 self.targetSize = -1
@ -224,6 +229,7 @@ class Side(ComposableText):
self.children: typing.MutableSequence[Module] = [] self.children: typing.MutableSequence[Module] = []
self.alignment = alignment self.alignment = alignment
self.bar = parent.getFirstParentOfType(Bar)
def generateMarkup(self) -> str: def generateMarkup(self) -> str:
if not self.children: if not self.children:
@ -234,22 +240,23 @@ class Side(ComposableText):
for section in module.getSections(): for section in module.getSections():
if section.isHidden(): if section.isHidden():
continue continue
hexa = section.color.get_truecolor(theme=self.bar.theme).hex
if lastSection is None: if lastSection is None:
if self.alignment == Alignment.LEFT: if self.alignment == Alignment.LEFT:
text += "%{B" + section.color + "}%{F-}" text += "%{B" + hexa + "}%{F-}"
else: else:
text += "%{B-}%{F" + section.color + "}%{R}%{F-}" text += "%{B-}%{F" + hexa + "}%{R}%{F-}"
else: else:
if self.alignment == Alignment.RIGHT: if self.alignment == Alignment.RIGHT:
if lastSection.color == section.color: if lastSection.color == section.color:
text += "" text += ""
else: else:
text += "%{F" + section.color + "}%{R}" text += "%{F" + hexa + "}%{R}"
else: else:
if lastSection.color == section.color: if lastSection.color == section.color:
text += "" text += ""
else: else:
text += "%{R}%{B" + section.color + "}" text += "%{R}%{B" + hexa + "}"
text += "%{F-}" text += "%{F-}"
text += section.getMarkup() text += section.getMarkup()
lastSection = section lastSection = section
@ -280,11 +287,15 @@ class Bar(ComposableText):
Top-level Top-level
""" """
def __init__(self) -> None: def __init__(
self,
theme: rich.terminal_theme.TerminalTheme = rich.terminal_theme.DEFAULT_TERMINAL_THEME,
) -> None:
super().__init__() super().__init__()
self.parent: None self.parent: None
self.children: typing.MutableSequence[Screen] self.children: typing.MutableSequence[Screen]
self.longRunningTasks: list[asyncio.Task] = list() self.longRunningTasks: list[asyncio.Task] = list()
self.theme = theme
self.refresh = asyncio.Event() self.refresh = asyncio.Event()
self.taskGroup = asyncio.TaskGroup() self.taskGroup = asyncio.TaskGroup()
@ -312,6 +323,10 @@ class Bar(ComposableText):
"64", "64",
"-f", "-f",
"DejaVuSansM Nerd Font:size=10", "DejaVuSansM Nerd Font:size=10",
"-F",
self.theme.foreground_color.hex,
"-B",
self.theme.background_color.hex,
] ]
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE *cmd, stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE
@ -380,8 +395,9 @@ class Bar(ComposableText):
class Provider: class Provider:
def __init__(self) -> None: def __init__(self, color: rich.color.Color = rich.color.Color.default()) -> None:
self.modules: list[Module] = list() self.modules: list[Module] = list()
self.color = color
async def run(self) -> None: async def run(self) -> None:
# Not a NotImplementedError, otherwise can't combine all classes # Not a NotImplementedError, otherwise can't combine all classes
@ -389,8 +405,8 @@ class Provider:
class MirrorProvider(Provider): class MirrorProvider(Provider):
def __init__(self) -> None: def __init__(self, color: rich.color.Color = rich.color.Color.default()) -> None:
super().__init__() super().__init__(color=color)
self.module: Module self.module: Module
async def run(self) -> None: async def run(self) -> None:
@ -403,11 +419,14 @@ class MirrorProvider(Provider):
class SingleSectionProvider(MirrorProvider): class SingleSectionProvider(MirrorProvider):
async def run(self) -> None: async def run(self) -> None:
await super().run() await super().run()
self.section = Section(parent=self.module) self.section = Section(parent=self.module, color=self.color)
class StaticProvider(SingleSectionProvider): class StaticProvider(SingleSectionProvider):
def __init__(self, text: str) -> None: def __init__(
self, text: str, color: rich.color.Color = rich.color.Color.default()
) -> None:
super().__init__(color=color)
self.text = text self.text = text
async def run(self) -> None: async def run(self) -> None:
@ -417,8 +436,13 @@ class StaticProvider(SingleSectionProvider):
class StatefulSection(Section): class StatefulSection(Section):
def __init__(self, parent: Module, sortKey: Sortable = 0) -> None: def __init__(
super().__init__(parent=parent, sortKey=sortKey) self,
parent: Module,
sortKey: Sortable = 0,
color: rich.color.Color = rich.color.Color.default(),
) -> None:
super().__init__(parent=parent, sortKey=sortKey, color=color)
self.state = 0 self.state = 0
self.numberStates: int self.numberStates: int
@ -444,7 +468,7 @@ class StatefulSection(Section):
class SingleStatefulSectionProvider(MirrorProvider): class SingleStatefulSectionProvider(MirrorProvider):
async def run(self) -> None: async def run(self) -> None:
await super().run() await super().run()
self.section = StatefulSection(parent=self.module) self.section = StatefulSection(parent=self.module, color=self.color)
class PeriodicProvider(Provider): class PeriodicProvider(Provider):
@ -518,8 +542,10 @@ class I3WindowTitleProvider(SingleSectionProvider):
class I3WorkspacesProvider(Provider): class I3WorkspacesProvider(Provider):
# TODO Custom names COLOR_URGENT = rich.color.Color.parse("red")
# TODO Colors COLOR_FOCUSED = rich.color.Color.parse("yellow")
COLOR_VISIBLE = rich.color.Color.parse("blue")
COLOR_DEFAULT = rich.color.Color.parse("bright_black")
async def updateWorkspaces(self, i3: i3ipc.Connection) -> None: async def updateWorkspaces(self, i3: i3ipc.Connection) -> None:
""" """
@ -554,11 +580,15 @@ class I3WorkspacesProvider(Provider):
) )
name = workspace.name name = workspace.name
if workspace.urgent: if workspace.urgent:
name = f"{name} !" section.color = self.COLOR_URGENT
elif workspace.focused: elif workspace.focused:
name = f"{name} +" section.color = self.COLOR_FOCUSED
elif workspace.visible: elif workspace.visible:
name = f"{name} *" section.color = self.COLOR_VISIBLE
else:
section.color = self.COLOR_DEFAULT
if workspace.focused or workspace.visible:
name = f"{name} X" # TODO Custom names
section.setText(name) section.setText(name)
workspacesNums = set(workspace.num for workspace in workspaces) workspacesNums = set(workspace.num for workspace in workspaces)
for num, section in self.sections.items(): for num, section in self.sections.items():
@ -595,8 +625,13 @@ class I3WorkspacesProvider(Provider):
class NetworkProviderSection(StatefulSection): class NetworkProviderSection(StatefulSection):
def __init__(self, parent: Module, iface: str, provider: "NetworkProvider") -> None: def __init__(
super().__init__(parent=parent, sortKey=iface) self,
parent: Module,
iface: str,
provider: "NetworkProvider",
) -> None:
super().__init__(parent=parent, sortKey=iface, color=provider.color)
self.iface = iface self.iface = iface
self.provider = provider self.provider = provider
@ -649,7 +684,7 @@ class NetworkProviderSection(StatefulSection):
net = ipaddress.IPv4Network( net = ipaddress.IPv4Network(
(address.address, address.netmask), strict=False (address.address, address.netmask), strict=False
) )
text += f" {net.with_prefixlen}" text += f" {address.address}/{net.prefixlen}"
break break
if state >= 3: # Speed if state >= 3: # Speed
@ -670,8 +705,11 @@ class NetworkProviderSection(StatefulSection):
class NetworkProvider(MirrorProvider, PeriodicProvider): class NetworkProvider(MirrorProvider, PeriodicProvider):
def __init__(self) -> None: def __init__(
super().__init__() self,
color: rich.color.Color = rich.color.Color.default(),
) -> None:
super().__init__(color=color)
self.sections: dict[str, NetworkProviderSection] = dict() self.sections: dict[str, NetworkProviderSection] = dict()
async def init(self) -> None: async def init(self) -> None:
@ -725,33 +763,86 @@ class TimeProvider(PeriodicStatefulProvider):
async def main() -> None: async def main() -> None:
bar = Bar() FROGARIZED = [
"#092c0e",
"#143718",
"#5a7058",
"#677d64",
"#89947f",
"#99a08d",
"#fae2e3",
"#fff0f1",
"#e0332e",
"#cf4b15",
"#bb8801",
"#8d9800",
"#1fa198",
"#008dd1",
"#5c73c4",
"#d43982",
]
# TODO Configurable
def base16_color(color: int) -> tuple[int, int, int]:
hexa = FROGARIZED[color]
return tuple(rich.color.parse_rgb_hex(hexa[1:]))
theme = rich.terminal_theme.TerminalTheme(
base16_color(0x0),
base16_color(0x0), # TODO should be 7, currently 0 so it's compatible with v2
[
base16_color(0x0), # black
base16_color(0x8), # red
base16_color(0xB), # green
base16_color(0xA), # yellow
base16_color(0xD), # blue
base16_color(0xE), # magenta
base16_color(0xC), # cyan
base16_color(0x5), # white
],
[
base16_color(0x3), # bright black
base16_color(0x8), # bright red
base16_color(0xB), # bright green
base16_color(0xA), # bright yellow
base16_color(0xD), # bright blue
base16_color(0xE), # bright magenta
base16_color(0xC), # bright cyan
base16_color(0x7), # bright white
],
)
bar = Bar(theme=theme)
dualScreen = len(bar.children) > 1 dualScreen = len(bar.children) > 1
bar.addProvider(I3ModeProvider(), alignment=Alignment.LEFT) color = rich.color.Color.parse
bar.addProvider(I3ModeProvider(color=color("red")), alignment=Alignment.LEFT)
bar.addProvider(I3WorkspacesProvider(), alignment=Alignment.LEFT) bar.addProvider(I3WorkspacesProvider(), alignment=Alignment.LEFT)
if dualScreen: if dualScreen:
bar.addProvider( bar.addProvider(
I3WindowTitleProvider(), screenNum=0, alignment=Alignment.CENTER I3WindowTitleProvider(), screenNum=0, alignment=Alignment.CENTER
) )
bar.addProvider( bar.addProvider(
StaticProvider(text="mpris"), StaticProvider(text="mpris", color=color("bright_white")),
screenNum=1 if dualScreen else None, screenNum=1 if dualScreen else None,
alignment=Alignment.CENTER, alignment=Alignment.CENTER,
) )
bar.addProvider(StaticProvider("C L M T B"), alignment=Alignment.RIGHT)
bar.addProvider( bar.addProvider(
StaticProvider("pulse"), StaticProvider("C L M T B", color=color("green")), alignment=Alignment.RIGHT
)
bar.addProvider(
StaticProvider("pulse", color=color("magenta")),
screenNum=1 if dualScreen else None, screenNum=1 if dualScreen else None,
alignment=Alignment.RIGHT, alignment=Alignment.RIGHT,
) )
bar.addProvider( bar.addProvider(
NetworkProvider(), NetworkProvider(color=color("blue")),
screenNum=0 if dualScreen else None, screenNum=0 if dualScreen else None,
alignment=Alignment.RIGHT, alignment=Alignment.RIGHT,
) )
bar.addProvider(TimeProvider(), alignment=Alignment.RIGHT) bar.addProvider(TimeProvider(color=color("cyan")), alignment=Alignment.RIGHT)
await bar.run() await bar.run()

View file

@ -25,13 +25,14 @@ pkgs.python3Packages.buildPythonApplication rec {
version = "2.0"; version = "2.0";
propagatedBuildInputs = with pkgs.python3Packages; [ propagatedBuildInputs = with pkgs.python3Packages; [
coloredlogs coloredlogs # old only
notmuch
i3ipc i3ipc
mpd2 mpd2
notmuch
psutil psutil
pulsectl pulsectl
pyinotify pyinotify
rich
]; ];
nativeBuildInputs = nativeBuildInputs =
[ lemonbar ] [ lemonbar ]