VoIPms-SMS-TUI/voipms-sms-tui.py

372 lines
15 KiB
Python

#!/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 Affero Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 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 Affero 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.settings = {
"user_left": 1
}
self.data = {
"1234567890": [
"Alice Bobson",
{
'id': 1,
'sender':'1111111111',
'timestamp':100, 'msg': "hi"
},{
'id': 2,
'sender':'1234567890',
'timestamp':200,
'msg': "Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi."
},{
'id': 3,
'sender':'1234567890',
'timestamp':300,
'msg': "wassup!"
},{
'id': 4,
'sender':'1111111111',
'timestamp':400,
'msg': "Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi. Oh, Hi."
}
],
"1112223333": [
"",
{
'id': 1,
'sender':'1111111111',
'timestamp':1,
'msg': "hello"
},{
'id': 2,
'sender':'1112223333',
'timestamp':2,
'msg': "oh, hi"
}
],
"0": [],
"1": [],
"2": [],
"3": [],
"4": [],
"5": [],
"6": [],
"7": [],
"8": [],
"9": [],
"10": [],
"11": [],
"12": [],
"13": [],
"14": [],
"15": [],
"16": [],
"17": [],
"18": [],
"19": [],
"20": [],
"21": [],
"22": [],
"23": [],
"24": [],
"25": [],
"26": [],
"27": [],
"28": [],
"29": [],
"30": [],
"31": [],
"32": [],
"33": [],
"34": [],
"35": [],
"36": [],
"37": [],
"38": [],
"39": [],
}
self.conversations = []
for key in self.data:
self.conversations.append(key)
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:
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['user_left'] == 1):
lines.append(
urwid.Columns(
[
urwid.Divider(" "),
(urwid.WEIGHT, 8, urwid.Padding(
urwid.LineBox(urwid.Text(m['msg'])), align='right', width=len(m['msg'])+2
)),
]
)
)
else:
lines.append(
urwid.Columns(
[
(urwid.WEIGHT, 8, urwid.Padding(
urwid.LineBox(urwid.Text(m['msg'])), width=len(m['msg'])+2
)),
urwid.Divider(" "),
]
)
)
return lines
class ConversationView(urwid.WidgetWrap):
palette = [
("body", "black", "light gray", "standout"),
("logo_red", "dark red", "light gray", "standout"),
("logo_black", "black", "light gray", "standout"),
("header", "white", "dark red", "bold"),
("footer", "black", "dark cyan"),
("sidebar", "dark gray", "black"),
("buttn", "white", "dark green"),
("buttnf", "white", "light green", "bold"),
]
def __init__(self, model):
self.model = model
self.started = True
self.current_conversation=None
super().__init__(self.main_window())
def logo(self):
return [
urwid.Divider(" ", top=10),
urwid.Padding(
urwid.Pile([
urwid.Text([("logo_red","┌─┐ ┌─┐┌──────┐┌─┐┌─────┐"),("logo_black","┌───────┐┌──────┐")],align=urwid.CENTER),
urwid.Text([("logo_red","│ │ │ ││ ┌──┐ │└─┘└───┐ │"),("logo_black","│ ┌───┐ ││ ┌────┘")],align=urwid.CENTER),
urwid.Text([("logo_red","│ └┐┌┘ ││ │ │ │┌─┐┌───┘ │"),("logo_black","│ │┌─┐│ ││ └┐┌──┐")],align=urwid.CENTER),
urwid.Text([("logo_red","└┐ └┘ ┌┘│ │ │ ││ ││ ┌───┘"),("logo_black","│ ││ ││ │└──┘└┐ │")],align=urwid.CENTER),
urwid.Text([("logo_red"," └┐ ┌┘ │ └──┘ ││ ││ │"),("logo_black"," ┌─┐│ ││ ││ │┌────┘ │")],align=urwid.CENTER),
urwid.Text([("logo_red"," └──┘ └──────┘└─┘└─┘"),("logo_black"," └─┘└─┘└─┘└─┘└──────┘")],align=urwid.CENTER),
urwid.Text([("logo_black","┌─────┐┌───────┐┌─────┐"),("logo_red"," ┌─────┐┌─┐ ┌─┐┌───┐")],align=urwid.CENTER),
urwid.Text([("logo_black","│ ┌───┘│ ┌┐ ┌┐ ││ ┌───┘"),("logo_red"," └─┐ ┌─┘│ │ │ │└┐ ┌┘")],align=urwid.CENTER),
urwid.Text([("logo_black","│ └───┐│ ││ ││ ││ └───┐┌─┐"),("logo_red","│ │ │ │ │ │ │ │ ")],align=urwid.CENTER),
urwid.Text([("logo_black","└───┐ ││ ││ ││ │└───┐ │└─┘"),("logo_red","│ │ │ │ │ │ │ │ ")],align=urwid.CENTER),
urwid.Text([("logo_black","┌───┘ ││ ││ ││ │┌───┘ │"),("logo_red"," │ │ │ └─┘ │┌┘ └┐")],align=urwid.CENTER),
urwid.Text([("logo_black","└─────┘└─┘└─┘└─┘└─────┘"),("logo_red"," └─┘ └─────┘└───┘")],align=urwid.CENTER),
]),
),
]
def update_conversation(self, force_update=False):
return self.model.get_messages()
def on_conversation_button(self, button, state):
"""Notify the model of a new conversation setting."""
self.scroll_to_bottom()
return 1
if state:
self.model.set_conversation(button.get_label().split("\n")[0])
self.model.set_conversation(self.conversation)
self.chat = self.conversation()
self.view = urwid.Frame(
urwid.AttrMap(self.chat, "body"),
footer=self.send
)
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
if m != "":
self.header = urwid.Text(m, urwid.CENTER)
else:
self.header = urwid.Text(self.current_conversation, urwid.CENTER)
conversation = self.model.get_conversations()[0]
def conversation(self):
if self.current_conversation == None:
return self.logo()
else:
return self.model.get_messages()
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.model.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 scroll_to_bottom(self):
self.scrollable.set_scrollpos(-1)
def main_window(self):
self.chat = self.conversation()
self.send_content = ""
self.send_edit = urwid.Edit("", self.send_content, multiline=True)
self.send_box = urwid.Padding(self.send_edit, left=1, right=1)
urwid.connect_signal(self.send_edit, 'change', self.send_update(self.send_content,self.send_edit))
self.send_button = urwid.Padding(
urwid.AttrMap(
urwid.Button(
"SEND", self.send_message(self.send_edit), align=urwid.CENTER
),
"buttn", "buttnf"
),
left=0,
right=0
)
self.char_count="0/140";
self.send_pile = urwid.Pile([
self.send_button,
urwid.Text(self.char_count, align=urwid.CENTER)
])
self.send = urwid.Columns([
(urwid.WEIGHT, 8, self.send_box),
self.send_pile,
])
self.header = urwid.Text("VoIP.ms SMS TUI", urwid.CENTER)
self.scrollable = urwid.Scrollable(urwid.Pile(self.chat))
self.sb_chat = urwid.ScrollBar(self.scrollable, trough_char=urwid.ScrollBar.Symbols.LITE_SHADE)
self.view = urwid.Pile([
('pack', urwid.AttrMap(self.header, "header")),
urwid.AttrMap(self.sb_chat, "body"),
('pack', urwid.AttrMap(self.send, "footer")),
]
)
self.scroll_to_bottom()
selection = self.conversation_selection()
sb_selection = urwid.ScrollBar(selection, trough_char=urwid.ScrollBar.Symbols.LITE_SHADE)
new_conversation = urwid.AttrMap(
urwid.Button(
"New", self.send_message(self.send_edit), align=urwid.CENTER
),
"buttn", "buttnf"
)
settings = urwid.AttrMap(
urwid.Button(
"Settings", self.send_message(self.send_edit), align=urwid.CENTER
),
"buttn", "buttnf"
)
c = urwid.Pile([
('pack',new_conversation),
sb_selection,
('pack',settings),
])
w = urwid.Columns(
[
urwid.AttrMap(c, "sidebar"),
(urwid.WEIGHT, 3, self.view)
],
dividechars=0, focus_column=0
)
w = urwid.AttrMap(w, "body")
return w
def main():
def get_conversations():
return model.get_conversations()
def set_conversation(m) -> None:
model.set_conversation(m)
view.update_conversation(True)
def get_messages():
return model.get_messages()
def unhandled( key: str | tuple[str, int, int, int]) -> None:
if key == "F8":
exit_program
model = ConversationModel()
view = ConversationView(model)
# use the first conversation as the default
conversation = get_conversations()[0]
model.set_conversation(conversation)
# update the view
view.on_conversation_change(conversation)
view.update_conversation(True)
urwid.MainLoop(view, view.palette, unhandled_input=unhandled).run()
view.scroll_to_bottom()
def setup():
main()
if __name__ == "__main__":
setup()