Basic application layout with urwid

This commit is contained in:
John Mertz 2024-02-29 22:40:16 -07:00
commit 7185ccfbc2
Signed by: jpm
GPG Key ID: E9C5EA2D867501AB
2 changed files with 256 additions and 0 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# VoIP.ms SMS TUI
TUI application for sending/receiving SMS messages using VoIP.ms API

253
voipms-sms-tui.py Normal file
View File

@ -0,0 +1,253 @@
#!/usr/bin/env python
#
# TUI for VoIP.ms SMS API
#
# Copyright (C) 2024 John Mertz <git@john.me.tz>
#
# 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()