From 7185ccfbc25a645f7a59d59f9fd32ed0a776f7e2 Mon Sep 17 00:00:00 2001 From: John Mertz Date: Thu, 29 Feb 2024 22:40:16 -0700 Subject: [PATCH] Basic application layout with urwid --- README.md | 3 + voipms-sms-tui.py | 253 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 README.md create mode 100644 voipms-sms-tui.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..299c1bc --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# VoIP.ms SMS TUI + +TUI application for sending/receiving SMS messages using VoIP.ms API diff --git a/voipms-sms-tui.py b/voipms-sms-tui.py new file mode 100644 index 0000000..7c6b36a --- /dev/null +++ b/voipms-sms-tui.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python +# +# TUI for VoIP.ms SMS API +# +# Copyright (C) 2024 John Mertz +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Source: https://git.john.me.tz/jpm/VoIPms-SMS-TUI + +from __future__ import annotations + +import typing +import urwid + +class ConversationModel: + + def __init__(self): + self.data = { + "1234567890": ["Alice Bobson", { 'id': 1, 'sender':'1111111111', 'timestamp':100, 'msg': "hi"},{ 'id': 2, 'sender':'1234567890', 'timestamp':200, 'msg': "wassup!"}], + "1112223333": ["", { 'id': 1, 'sender':'1111111111', 'timestamp':1, 'msg': "hello"},{ 'id': 2, 'sender':'1112223333', 'timestamp':2, 'msg': "oh, hi"}], + } + self.conversations = [] + for key in self.data: + self.conversations.append(key) + self.current_conversation=self.conversations[0] + self.settings = { + "me_left": 1 + } + + def get_conversations(self): + return self.conversations + + def set_conversation(self, m) -> None: + self.current_conversation = m + + def get_messages(self): + lines = [] + d = self.data[self.current_conversation] + last_time = 0 + for m in d: + if not last_time: + if m != "": + lines.append(urwid.AttrMap(urwid.Text(m, urwid.CENTER), "header")) + else: + lines.append(urwid.Text(self.current_conversation, urwid.CENTER)) + last_time=1 + continue + if m['timestamp']-last_time >= 10: + lines.append(urwid.Text(str(m['timestamp']), urwid.CENTER)) + last_time = m['timestamp'] + if (m['sender'] == self.current_conversation) ^ (self.settings['me_left'] == 1): + lines.append(urwid.Padding(urwid.Text(m['msg'], urwid.RIGHT),left=10,right=1)) + else: + lines.append(urwid.Padding(urwid.Text(m['msg'], urwid.LEFT),left=1,right=10)) + return lines + +class ConversationView(urwid.WidgetWrap): + """ + A class responsible for providing the application's interface and + graph display. + """ + + palette: typing.ClassVar[tuple[str, str, str, ...]] = [ + ("body", "black", "light gray", "standout"), + ("header", "white", "dark red", "bold"), + ("screen edge", "light blue", "dark cyan"), + ("main shadow", "dark gray", "black"), + ("line", "black", "light gray", "standout"), + ("bg background", "light gray", "black"), + ("bg 1", "black", "dark blue", "standout"), + ("bg 1 smooth", "dark blue", "black"), + ("bg 2", "black", "dark cyan", "standout"), + ("bg 2 smooth", "dark cyan", "black"), + ("button normal", "light gray", "dark blue", "standout"), + ("button select", "white", "dark green"), + ("line", "black", "light gray", "standout"), + ("pg normal", "white", "black", "standout"), + ("pg complete", "white", "dark magenta"), + ("pg smooth", "dark magenta", "black"), + ] + + graph_samples_per_bar = 10 + graph_num_bars = 5 + graph_offset_per_second = 5 + + + def __init__(self, controller): + self.controller = controller + self.started = True + self.offset = 0 + self.last_offset = None + super().__init__(self.main_window()) + + def update_conversation(self, force_update=False): + return urwid.SimpleListWalker(self.controller.get_messages()) + + def on_conversation_button(self, button, state): + """Notify the controller of a new conversation setting.""" + if state: + # The new conversation is the label of the button + self.controller.set_conversation(button.get_label().split("\n")[0]) + self.model.set_conversation(conversation) + self.graph = self.conversation() + self.view = urwid.Frame( + urwid.AttrMap(self.graph, "body"), + footer=self.send + ) + self.last_offset = None + + def on_conversation_change(self, m): + """Handle external conversation change by updating radio buttons.""" + for rb in self.conversation_buttons: + if rb.base_widget.label == m: + rb.base_widget.set_state(True, do_callback=False) + break + self.last_offset = None + conversation = self.controller.get_conversations()[0] + + def bar_graph(self, smooth=False): + satt = None + if smooth: + satt = {(1, 0): "bg 1 smooth", (2, 0): "bg 2 smooth"} + w = urwid.BarGraph(["bg background", "bg 1", "bg 2"], satt=satt) + return w + + def conversation(self): + return urwid.ListBox(self.update_conversation()) + + def button(self, t, fn): + w = urwid.Button(t, fn) + w = urwid.AttrMap(w, "button normal", "button select") + return w + + def radio_button(self, g, label, fn): + w = urwid.RadioButton(g, label+"\nName: "+label, False, on_state_change=fn) + #w = urwid.RadioButton(g, label+"\nName: "+self.get_name(label), False, on_state_change=fn) + w = urwid.AttrMap(w, "button normal", "button select") + return w + + def get_name(self, name): + return self.model.data[name][0] + + def exit_program(self, w): + raise urwid.ExitMainLoop() + + def conversation_selection(self): + conversations = self.controller.get_conversations() + # setup conversation radio buttons + self.conversation_buttons = [] + group = [] + for m in conversations: + rb = self.radio_button(group, m, self.on_conversation_button) + self.conversation_buttons.append(rb) + + lines = [ + *self.conversation_buttons, + ] + w = urwid.ListBox(urwid.SimpleListWalker(lines)) + return w + + def send_message(self, msg): + print("Send button pressed: "+msg.edit_text) + + def send_update(self, field, msg): + field=msg.edit_text + + def main_window(self): + #self.graph = self.bar_graph() + self.graph = self.conversation() + self.send_content = "preset" + self.send_box = urwid.Edit("", self.send_content, multiline=True) + urwid.connect_signal(self.send_box, 'change', self.send_update(self.send_content,self.send_box)) + self.send_button = urwid.Padding( + urwid.AttrMap( + urwid.Button( + "SEND", self.send_message(self.send_box), align=urwid.CENTER + ), + "buttn", "buttnf" + ), + left=0, + right=0 + ) + self.send = urwid.Columns([ + (urwid.WEIGHT, 8, self.send_box), + self.send_button, + ]) + self.view = urwid.Frame( + urwid.AttrMap(self.graph, "body"), + footer=self.send + ) + c = self.conversation_selection() + w = urwid.Columns([c, (urwid.WEIGHT, 3, self.view)], dividechars=0, focus_column=0) + w = urwid.AttrMap(w, "body") + return w + + +class SMSController: + """ + A class responsible for setting up the model and view and running + the application. + """ + + def __init__(self): + self.model = ConversationModel() + self.view = ConversationView(self) + # use the first conversation as the default + conversation = self.get_conversations()[0] + self.model.set_conversation(conversation) + # update the view + self.view.on_conversation_change(conversation) + self.view.update_conversation(True) + + def get_conversations(self): + """Allow our view access to the list of conversations.""" + return self.model.get_conversations() + + def set_conversation(self, m) -> None: + """Allow our view to set the conversation.""" + self.model.set_conversation(m) + self.view.update_conversation(True) + + def get_messages(self): + """Provide data to our view for the graph.""" + return self.model.get_messages() + + def unhandled(self, key: str | tuple[str, int, int, int]) -> None: + if key == "F8": + self.exit_program + + def main(self): + urwid.MainLoop(self.view, self.view.palette, unhandled_input=self.unhandled).run() + self.loop.run() + + +def main(): + SMSController().main() + + +if __name__ == "__main__": + main()