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> # Copyright (C) 2024 John Mertz <git@john.me.tz>
# #
# This library is free software; you can redistribute it and/or # 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 # 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, # This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details. # 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 # License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# #
@ -28,17 +28,91 @@ import urwid
class ConversationModel: class ConversationModel:
def __init__(self): def __init__(self):
self.settings = {
"user_left": 1
}
self.data = { self.data = {
"1234567890": ["Alice Bobson", { 'id': 1, 'sender':'1111111111', 'timestamp':100, 'msg': "hi"},{ 'id': 2, 'sender':'1234567890', 'timestamp':200, 'msg': "wassup!"}], "1234567890": [
"1112223333": ["", { 'id': 1, 'sender':'1111111111', 'timestamp':1, 'msg': "hello"},{ 'id': 2, 'sender':'1112223333', 'timestamp':2, 'msg': "oh, hi"}], "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 = [] self.conversations = []
for key in self.data: for key in self.data:
self.conversations.append(key) self.conversations.append(key)
self.current_conversation=self.conversations[0]
self.settings = {
"me_left": 1
}
def get_conversations(self): def get_conversations(self):
return self.conversations return self.conversations
@ -52,73 +126,90 @@ class ConversationModel:
last_time = 0 last_time = 0
for m in d: for m in d:
if not last_time: 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 last_time=1
continue continue
if m['timestamp']-last_time >= 10: if m['timestamp']-last_time >= 10:
lines.append(urwid.Text(str(m['timestamp']), urwid.CENTER)) lines.append(urwid.Text(str(m['timestamp']), urwid.CENTER))
last_time = m['timestamp'] last_time = m['timestamp']
if (m['sender'] == self.current_conversation) ^ (self.settings['me_left'] == 1): if (m['sender'] == self.current_conversation) ^ (self.settings['user_left'] == 1):
lines.append(urwid.Padding(urwid.Text(m['msg'], urwid.RIGHT),left=10,right=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: 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 return lines
class ConversationView(urwid.WidgetWrap): 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"), ("body", "black", "light gray", "standout"),
("logo_red", "dark red", "light gray", "standout"),
("logo_black", "black", "light gray", "standout"),
("header", "white", "dark red", "bold"), ("header", "white", "dark red", "bold"),
("screen edge", "light blue", "dark cyan"), ("footer", "black", "dark cyan"),
("main shadow", "dark gray", "black"), ("sidebar", "dark gray", "black"),
("line", "black", "light gray", "standout"), ("buttn", "white", "dark green"),
("bg background", "light gray", "black"), ("buttnf", "white", "light green", "bold"),
("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 def __init__(self, model):
graph_num_bars = 5 self.model = model
graph_offset_per_second = 5
def __init__(self, controller):
self.controller = controller
self.started = True self.started = True
self.offset = 0 self.current_conversation=None
self.last_offset = None
super().__init__(self.main_window()) 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): 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): 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: if state:
# The new conversation is the label of the button self.model.set_conversation(button.get_label().split("\n")[0])
self.controller.set_conversation(button.get_label().split("\n")[0]) self.model.set_conversation(self.conversation)
self.model.set_conversation(conversation) self.chat = self.conversation()
self.graph = self.conversation()
self.view = urwid.Frame( self.view = urwid.Frame(
urwid.AttrMap(self.graph, "body"), urwid.AttrMap(self.chat, "body"),
footer=self.send footer=self.send
) )
self.last_offset = None
def on_conversation_change(self, m): def on_conversation_change(self, m):
"""Handle external conversation change by updating radio buttons.""" """Handle external conversation change by updating radio buttons."""
@ -126,18 +217,17 @@ class ConversationView(urwid.WidgetWrap):
if rb.base_widget.label == m: if rb.base_widget.label == m:
rb.base_widget.set_state(True, do_callback=False) rb.base_widget.set_state(True, do_callback=False)
break break
self.last_offset = None if m != "":
conversation = self.controller.get_conversations()[0] self.header = urwid.Text(m, urwid.CENTER)
else:
def bar_graph(self, smooth=False): self.header = urwid.Text(self.current_conversation, urwid.CENTER)
satt = None conversation = self.model.get_conversations()[0]
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): 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): def button(self, t, fn):
w = urwid.Button(t, fn) w = urwid.Button(t, fn)
@ -157,7 +247,7 @@ class ConversationView(urwid.WidgetWrap):
raise urwid.ExitMainLoop() raise urwid.ExitMainLoop()
def conversation_selection(self): def conversation_selection(self):
conversations = self.controller.get_conversations() conversations = self.model.get_conversations()
# setup conversation radio buttons # setup conversation radio buttons
self.conversation_buttons = [] self.conversation_buttons = []
group = [] group = []
@ -177,77 +267,105 @@ class ConversationView(urwid.WidgetWrap):
def send_update(self, field, msg): def send_update(self, field, msg):
field=msg.edit_text field=msg.edit_text
def scroll_to_bottom(self):
self.scrollable.set_scrollpos(-1)
def main_window(self): def main_window(self):
#self.graph = self.bar_graph() self.chat = self.conversation()
self.graph = self.conversation() self.send_content = ""
self.send_content = "preset" self.send_edit = urwid.Edit("", self.send_content, multiline=True)
self.send_box = urwid.Edit("", self.send_content, multiline=True) self.send_box = urwid.Padding(self.send_edit, left=1, right=1)
urwid.connect_signal(self.send_box, 'change', self.send_update(self.send_content,self.send_box)) urwid.connect_signal(self.send_edit, 'change', self.send_update(self.send_content,self.send_edit))
self.send_button = urwid.Padding( self.send_button = urwid.Padding(
urwid.AttrMap( urwid.AttrMap(
urwid.Button( urwid.Button(
"SEND", self.send_message(self.send_box), align=urwid.CENTER "SEND", self.send_message(self.send_edit), align=urwid.CENTER
),
"buttn", "buttnf"
), ),
"buttn", "buttnf"
),
left=0, left=0,
right=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([ self.send = urwid.Columns([
(urwid.WEIGHT, 8, self.send_box), (urwid.WEIGHT, 8, self.send_box),
self.send_button, self.send_pile,
]) ])
self.view = urwid.Frame( self.header = urwid.Text("VoIP.ms SMS TUI", urwid.CENTER)
urwid.AttrMap(self.graph, "body"), self.scrollable = urwid.Scrollable(urwid.Pile(self.chat))
footer=self.send 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") w = urwid.AttrMap(w, "body")
return w 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(): 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__": if __name__ == "__main__":
main() setup()