Compare commits

...

7 Commits

3 changed files with 241 additions and 119 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "submodules/VoIPms-SMS"]
path = submodules/VoIPms-SMS
url = ssh://git@git.john.me.tz:223/jpm/VoIPms-SMS.git

1
submodules/VoIPms-SMS Submodule

@ -0,0 +1 @@
Subproject commit 307adb25e57526103afc955ea3553daab1ad82eb

View File

@ -5,16 +5,16 @@
# 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
# modify it under the terms of the GNU Affero 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.
# 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 Lesser General Public
# 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
#
@ -28,17 +28,91 @@ 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': "wassup!"}],
"1112223333": ["", { 'id': 1, 'sender':'1111111111', 'timestamp':1, 'msg': "hello"},{ 'id': 2, 'sender':'1112223333', 'timestamp':2, 'msg': "oh, hi"}],
"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)
self.current_conversation=self.conversations[0]
self.settings = {
"me_left": 1
}
def get_conversations(self):
return self.conversations
@ -52,73 +126,90 @@ class ConversationModel:
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))
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.Padding(urwid.Text(m['msg'], urwid.LEFT),left=1,right=10))
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):
"""
A class responsible for providing the application's interface and
graph display.
"""
palette: typing.ClassVar[tuple[str, str, str, ...]] = [
palette = [
("body", "black", "light gray", "standout"),
("logo_red", "dark red", "light gray", "standout"),
("logo_black", "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"),
("footer", "black", "dark cyan"),
("sidebar", "dark gray", "black"),
("buttn", "white", "dark green"),
("buttnf", "white", "light green", "bold"),
]
graph_samples_per_bar = 10
graph_num_bars = 5
graph_offset_per_second = 5
def __init__(self, controller):
self.controller = controller
def __init__(self, model):
self.model = model
self.started = True
self.offset = 0
self.last_offset = None
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 urwid.SimpleListWalker(self.controller.get_messages())
return self.model.get_messages()
def on_conversation_button(self, button, state):
"""Notify the controller of a new conversation setting."""
"""Notify the model of a new conversation setting."""
self.scroll_to_bottom()
return 1
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.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.graph, "body"),
urwid.AttrMap(self.chat, "body"),
footer=self.send
)
self.last_offset = None
def on_conversation_change(self, m):
"""Handle external conversation change by updating radio buttons."""
@ -126,18 +217,17 @@ class ConversationView(urwid.WidgetWrap):
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
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):
return urwid.ListBox(self.update_conversation())
if self.current_conversation == None:
return self.logo()
else:
return self.model.get_messages()
def button(self, t, fn):
w = urwid.Button(t, fn)
@ -157,7 +247,7 @@ class ConversationView(urwid.WidgetWrap):
raise urwid.ExitMainLoop()
def conversation_selection(self):
conversations = self.controller.get_conversations()
conversations = self.model.get_conversations()
# setup conversation radio buttons
self.conversation_buttons = []
group = []
@ -177,77 +267,105 @@ class ConversationView(urwid.WidgetWrap):
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.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.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_box), align=urwid.CENTER
),
"buttn", "buttnf"
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_button,
self.send_pile,
])
self.view = urwid.Frame(
urwid.AttrMap(self.graph, "body"),
footer=self.send
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
)
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()
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__":
main()
setup()