381 lines
12 KiB
Python
Executable File
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()
|