pywmreceived/pywmreceived.py

381 lines
12 KiB
Python
Executable File

#!./venv/bin/python3
"""pywmreceived.py
WindowMaker dockapp pidgin messages
Copyright (C) 2025 Fredrick W. Warren
Licensed under the GNU General Public License.
"""
import click
import configparser
import dbus
import dbus.mainloop.glib
import logging
import os.path
import threading
from gi.repository import GLib
from icecream import ic
from wmdocklib import wmoo as wmoo
from xpm_resources import palette, background, patterns
line_height = 9
config_file = os.path.expanduser("~/.config") + '/pywmreceived/config.ini'
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def load_config(config_path):
"""
Load configuration from the specified config file.
Parameters:
config_path (str): Path to the configuration file
Returns:
list: List of configured lines with name, message count, and accounts
"""
# Default configuration (used if file doesn't exist)
default_lines = [
[" ONE", 0, ['one@example.com']],
[" TWO", 0, ['two@example.com']],
[" THREE", 0, ['three@example.com']],
[" FOUR", 0, ['four@example.com']],
[" FIVE", 0, ['five@exmaple.com']],
]
config = configparser.ConfigParser()
if not os.path.exists(config_path):
logger.warning(f"Config file not found at {config_path}. Using default configuration.")
os.makedirs(os.path.dirname(config_path), exist_ok=True)
config['DEFAULT'] = {'max_lines': '5'} #limit to 5 from config
for i, line in enumerate(default_lines):
section = f'line{i+1}'
config[section] = {
'name': line[0].strip(),
'accounts': ','.join(line[2])
}
with open(config_path, 'w') as configfile:
config.write(configfile)
result = [ [line[0], line[1], line[2]] for line in default_lines]
result.append([" OTHER", 0, ['']])
return result
try:
config.read(config_path)
max_lines = int(config['DEFAULT'].get('max_lines', '5')) #get max lines from config, default to 5.
configured_lines = []
for i in range(1, max_lines + 1):
section = f'line{i}'
if section in config:
name = ' ' + config[section].get('name', '').strip()
accounts_str = config[section].get('accounts', '')
accounts = [acc.strip() for acc in accounts_str.split(',') if acc.strip()]
configured_lines.append([name, 0, accounts])
else:
break #stop if no more config sections.
configured_lines.append([" OTHER", 0, ['']]) #always add OTHER
return configured_lines
except (configparser.Error, ValueError) as e:
logger.error(f"Error reading config file: {e}. Using default configuration and adding OTHER.")
result = [ [line[0], line[1], line[2]] for line in default_lines]
result.append([" OTHER", 0, ['']])
return result
# Read config file
config.read(config_path)
# Parse configuration
lines = []
# Get number of sections (excluding DEFAULT)
sections = [s for s in config.sections() if s.startswith('line')]
for section in sorted(sections):
if section.startswith('line'):
name = config[section].get('name', 'UNKNOWN')
accounts_str = config[section].get('accounts', '')
# Parse accounts - split by comma and strip whitespace
accounts = [account.strip() for account in accounts_str.split(',') if account.strip()]
# Add to lines with proper formatting (two spaces before name)
lines.append([f" {name}", 0, accounts])
# Always ensure we have the OTHER entry as the last one
if not lines or lines[-1][0].strip() != "OTHER":
lines.append([" OTHER", 0, ['']])
return lines
class Application(wmoo.Application):
"""
Display dockapp and respond to libpurple dbus
messages ReceivedImMsg and SentImMsg
"""
def __init__(self, *args, **kwargs):
"""
Initialize the application
"""
# must remove config_path before passing **kwargs to woo.Application
config_path = kwargs.pop('config_path', config_file)
wmoo.Application.__init__(self, *args, **kwargs)
self._count = 0
self._flasher = 0
self.backlit = 0
# Load configuration from file
self.lines = load_config(config_path)
# Initialize D-Bus and connect to Pidgin's ReceivedIMMsg signal
self.register_dbus()
def register_dbus(self):
"""
Register im.pidgin.purple dbus to listen for messages
"""
try:
# Set up the D-Bus main loop
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
# Connect to the session bus
bus = dbus.SessionBus()
# Obtain the Pidgin D-Bus service object
purple_service = bus.get_object(
"im.pidgin.purple.PurpleService",
"/im/pidgin/purple/PurpleObject"
)
# Get the interface to interact with
purple_interface = dbus.Interface(
purple_service,
"im.pidgin.purple.PurpleInterface"
)
# Connect the ReceivedIMMsg signal to the handler
purple_interface.connect_to_signal(
"ReceivedImMsg",
self.handle_received_im_msg
)
# Connect the SentIMMsg signal to the handler
purple_interface.connect_to_signal(
"SentImMsg",
self.handle_sent_im_msg
)
ic("Connected to Pidgin's ReceivedIMMsg signal successfully.")
except dbus.DBusException as e:
print("Failed to connect to Pidgin's D-Bus interface:", e)
def handle_received_im_msg(self, account, sender, message, conversation, flags):
"""
Callback function that handles the ReceivedIMMsg signal.
Prints the sender and message.
Parameters:
account (str): The account from which the message was received.
sender (str): The sender's identifier.
message (str): The message content.
conversation (str): The conversation identifier.
flags (int): Message flags.
"""
# find who the message is for or return OTHER
self.lines[-1][0] = '* OTHER'
for line in self.lines[:-1]:
if sender.split('/')[0] in line[2]:
line[0] = '*' + line[0][1:]
self.lines[-1][0] = ' OTHER'
ic("")
ic(f"sender: {sender}")
ic(f"message: {message}")
if self.backlit:
self._flasher = 8
else:
self._flasher = 7
def handle_sent_im_msg(self, account, recepient, message):
"""
Callback function that handles the SentImMsg signal.
Prints the sender and message.
Parameters:
recepient (str): The recepien's identifier.
message (str): The message content.
"""
ic("")
ic(f"recepient: {recepient}")
ic(f"message: {message}")
self.clear_messages()
def draw_string(self, xstart, ystart, text):
"""
Draw text in dockapp with normal or backlit backround
Parameters:
xstart (int): pixels from left edge of dockapp
ystart (int): pixels from top edge of dockapp
text (str): text to display, will be trundicated
"""
for char in text:
if char >= "A" and char <="Z":
x = (ord(char) -65) * 6
y = 1
elif char >= "0" and char <="9":
x = (ord(char) -48) * 6
y = 10
elif char == " ":
x = 6 * 10
y = 10
elif char == "-":
x = 6 * 11
y = 10
elif char == ".":
x = 6 * 12
y = 10
elif char == "'":
x = 6 * 13
y = 10
elif char == "(":
x = 6 * 14
y = 10
elif char == ")":
x = 6 * 15
y = 10
elif char == "*":
x = 6 * 16
y = 10
elif char == "/":
x = 6 * 17
y = 10
else:
continue
self.putPattern(x, y + (self.backlit * 17), 6, 7, xstart, ystart)
xstart += 6
def draw_background(self):
"""
Redraw background of dockapp
"""
self.putPattern(0 + (self.backlit * 62), 36, 64, 64, 0, 0)
def draw_all_text(self):
"""
Redraw all text
"""
for index, line in enumerate(self.lines[:6]):
self.draw_string(9, 6 + (index * line_height), line[0])
def toggle_backlite(self):
"""
Toggle the state of the dockapp background
"""
self.backlit = 1 - self.backlit
self.draw_background()
self.draw_all_text()
def backlite_off(self):
"""
Turn off the backlight mode in response to a mouseclick
"""
self._flasher = 0
if self.backlit:
self.toggle_backlite()
def update(self):
"""
Update display
"""
wmoo.Application.update(self)
self._count += 1
if self._count <= 3:
return
self._count = 0
if self._flasher:
self._flasher -= 1
self.toggle_backlite()
def clear_messages(self):
"""
Turn of backlite and zero out messages
"""
# clear out text
for line in self.lines:
line[0] = ' ' + line[0][1:]
self.backlite_off()
self.update()
def handle_buttonrelease(self, event):
"""
On left click zero out recieved messages and turn off backlite
Parameters:
event (dict):
button (int): 1 left click, 2 middle click, 3 right click
type (str): buttonrelease
x (int): x position of click
y (int): y position of click
"""
if event['button'] == 1:
self.clear_messages()
def run_glib_mainloop():
"""
Runs the GLib main loop. This should be executed in a separate thread.
"""
loop = GLib.MainLoop()
ic("Start Loop")
try:
loop.run()
except KeyboardInterrupt:
loop.quit()
@click.command()
@click.option('--config', '-c', default=config_file, help='Path to the configuration file.')
@click.option('--debug/-d', envvar='DEVELOPMENT', default='false', help='Enable debug output')
def main(config, debug):
"""
The main entry point of the application.
Parameters:
config (str): path to config file
debug (bool): is debug output enabled
"""
if not debug:
ic.disable()
app = Application(font_name='5x8',
margin = 3,
bg=0,
fg=2,
palette = palette,
background = background,
patterns = patterns,
config_path = config)
# app.addCallback(app.previousRadio, 'buttonrelease', area=( 6,29,15,38))
# 6x7 grey1=1 grey2=10 green1=18 green2=27
app.draw_background()
app.draw_all_text()
app.addCallback(app.handle_buttonrelease, 'buttonrelease', area=(2,2,62,62))
# Start the GLib main loop in a separate thread
glib_thread = threading.Thread(target=run_glib_mainloop, daemon=True)
glib_thread.start()
# Run the application's main loop
app.run()
if __name__ == '__main__':
main()