Merge branch 'patch_readline' into dev_readline

This commit is contained in:
Mark Qvist
2026-05-25 21:38:26 +02:00
9 changed files with 345 additions and 105 deletions
+2 -4
View File
@@ -15,6 +15,7 @@ from nomadnet.vendor.Scrollable import *
from nomadnet.util import strip_modifiers
from nomadnet.util import sanitize_name
from .Helpers import ClickableIcon, osc52_copy
from .ReadlineEdit import ReadlineMixin, ReadlineEdit
class BrowserFrame(urwid.Frame):
def keypress(self, size, key):
@@ -1829,12 +1830,9 @@ class UrlDialogLineBox(urwid.LineBox):
else:
return super(UrlDialogLineBox, self).keypress(size, key)
class UrlEdit(urwid.Edit):
class UrlEdit(ReadlineMixin, urwid.Edit):
def keypress(self, size, key):
if key == "enter":
self.confirmed(self)
elif key == "ctrl k":
self.set_edit_text("")
self.set_edit_pos(0)
else:
return super(UrlEdit, self).keypress(size, key)
+52 -41
View File
@@ -13,6 +13,7 @@ from nomadnet.ui.textui.MicronParser import LinkableText, LinkSpec
from RNS.Utilities.rngit.util import MarkdownToMicron
from RNS.Utilities.rngit.highlight import SyntaxHighlighter
from .MicronParser import markup_to_attrmaps, default_state, make_style
from .ReadlineEdit import ReadlineMixin, ReadlineEdit
from nomadnet.util import sanitize_name, strip_modifiers, strip_micron
from nomadnet.util import strip_escaped_micron, unescape_micron, strip_non_formatting_tags
from nomadnet.vendor.Scrollable import Scrollable, ScrollBar
@@ -191,13 +192,19 @@ def _format_ts(ts_ms):
class ChannelsListShortcuts():
def __init__(self, app):
self.app = app
self.widget = urwid.AttrMap(urwid.Text("[C-n] New Hub [C-a] Add Room [C-r] Connect [C-w] Disconnect [C-t] Auto-reconnect [C-e] Edit Hub [C-x] Remove [C-y] Toggle Channels"), "shortcutbar")
self.widget = urwid.AttrMap(urwid.Text("[C-n] New Hub [C-a] Add Room [C-r] Connect [C-w] Disconnect [C-t] Auto-reconnect [C-e] Edit Hub [C-x] Remove"), "shortcutbar")
class ChannelsRoomShortcuts():
def __init__(self, app):
self.app = app
self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-w] Leave [C-k] Clear [C-u] Users [C-y] Channels [F8] Collapse Joins [Tab] Focus"), "shortcutbar")
self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-x] Leave [F8] Collapse [Tab] Complete Nick"), "shortcutbar")
class ChannelsRoomBodyShortcuts():
def __init__(self, app):
self.app = app
self.widget = urwid.AttrMap(urwid.Text("[C-x] Leave [C-u] Users [C-y] Channels [F8] Collapse Joins [Tab] ↓ Editor"), "shortcutbar")
class ChannelsDialogLineBox(urwid.LineBox):
@@ -381,7 +388,7 @@ class HubInfoArea(urwid.LineBox):
return super(HubInfoArea, self).keypress(size, key)
class RoomMessageEdit(urwid.Edit):
class RoomMessageEdit(ReadlineMixin, urwid.Edit):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._tab_state = None
@@ -394,14 +401,8 @@ class RoomMessageEdit(urwid.Edit):
self._tab_state = None
if key == "ctrl d":
self.delegate.send_message()
elif key == "ctrl k":
self.set_edit_text("")
elif key == "ctrl w":
elif key == "ctrl x":
self.delegate.leave_room()
elif key == "ctrl u":
self.delegate.toggle_users()
elif key == "ctrl y":
self.delegate.display.toggle_channel_list()
elif key == "f8":
self.delegate.display.toggle_join_part_collapse()
elif key == "up":
@@ -484,34 +485,37 @@ class RoomMessageEdit(urwid.Edit):
class RoomFrame(urwid.Frame):
@property
def focus_position(self):
return urwid.Frame.focus_position.fget(self)
@focus_position.setter
def focus_position(self, part):
urwid.Frame.focus_position.fset(self, part)
try:
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.update_active_shortcuts()
except Exception:
pass
def keypress(self, size, key):
if key == "ctrl u":
self.delegate.toggle_users()
return None
if key == "ctrl y":
self.delegate.display.toggle_channel_list()
return None
if key == "f8":
self.delegate.display.toggle_join_part_collapse()
return None
if key == "tab":
rw = self.delegate
users_focusable = (
rw is not None
and getattr(rw, "users_visible", False)
and len(rw.columns.contents) > 1
and rw.columns.contents[1][0] is rw.users_box
)
if self.focus_position == "body":
if key in ("ctrl u", "ctrl x", "ctrl y", "f8", "tab"):
result = super(RoomFrame, self).keypress(size, key)
if result != key:
return result
if key == "ctrl u":
self.delegate.toggle_users()
return None
if key == "ctrl x":
self.delegate.leave_room()
return None
if key == "ctrl y":
self.delegate.display.toggle_channel_list()
return None
if key == "f8":
self.delegate.display.toggle_join_part_collapse()
return None
if self.focus_position != "footer":
self.focus_position = "footer"
elif users_focusable:
try:
rw.columns.focus_position = 1
return None
except Exception:
self.focus_position = "body"
else:
self.focus_position = "body"
return None
elif self.focus_position == "body":
if key == "down" and getattr(self.delegate, "messagelist", None) is not None and self.delegate.messagelist.bottom_is_visible:
@@ -1427,6 +1431,7 @@ class ChannelsDisplay():
self.list_shortcuts = ChannelsListShortcuts(self.app)
self.room_shortcuts = ChannelsRoomShortcuts(self.app)
self.room_body_shortcuts = ChannelsRoomBodyShortcuts(self.app)
self.shortcuts_display = self.list_shortcuts
self.placeholder = urwid.LineBox(urwid.Filler(urwid.Text("\n Select or add a hub to begin", align=urwid.CENTER), "top"))
@@ -1549,6 +1554,12 @@ class ChannelsDisplay():
except Exception:
focus_path = None
if focus_path and focus_path[0] == 1 and self.current_room_widget is not None:
try:
frame = self.current_room_widget.frame
if frame is not None and frame.focus_position == "body":
return self.room_body_shortcuts
except Exception:
pass
return self.room_shortcuts
return self.list_shortcuts
@@ -1886,8 +1897,8 @@ class ChannelsDisplay():
self._show_dialog_overlay(dialog)
def new_hub_dialog(self):
e_hash = urwid.Edit(caption="Hub address : ", edit_text="")
e_name = urwid.Edit(caption="Display name: ", edit_text="")
e_hash = ReadlineEdit(caption="Hub address : ", edit_text="")
e_name = ReadlineEdit(caption="Display name: ", edit_text="")
error_text = urwid.Text("")
def dismiss(sender):
@@ -1975,7 +1986,7 @@ class ChannelsDisplay():
return
hub = item.hub
e_name = urwid.Edit(caption="Display name : ", edit_text=hub.name or "")
e_name = ReadlineEdit(caption="Display name : ", edit_text=hub.name or "")
cb_autorcn = urwid.CheckBox("Auto-reconnect on disconnect", state=hub.auto_reconnect)
cb_autolist = urwid.CheckBox("Auto-fetch room list on connect", state=hub.auto_list)
cb_autowho = urwid.CheckBox("Auto-fetch members on room join", state=hub.auto_who)
@@ -2032,8 +2043,8 @@ class ChannelsDisplay():
else:
return
e_room = urwid.Edit(caption="Room : #", edit_text="")
e_key = urwid.Edit(caption="Key : ", edit_text="", mask="*")
e_room = ReadlineEdit(caption="Room : #", edit_text="")
e_key = ReadlineEdit(caption="Key : ", edit_text="", mask="*")
error_text = urwid.Text("")
key_section_placeholder = urwid.WidgetPlaceholder(urwid.Text(""))
+47 -23
View File
@@ -21,6 +21,7 @@ from RNS.Utilities.rngit.util import MarkdownToMicron
from RNS.Utilities.rngit.highlight import SyntaxHighlighter
from .MicronParser import markup_to_attrmaps
from .Helpers import ClickableIcon, osc52_copy
from .ReadlineEdit import ReadlineMixin, ReadlineEdit
from nomadnet.util import strip_modifiers, strip_micron, strip_escaped_micron, unescape_micron, strip_non_formatting_tags
from nomadnet.ui import THEME_DARK, THEME_LIGHT
@@ -70,7 +71,13 @@ class ConversationDisplayShortcuts():
def __init__(self, app):
self.app = app
self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-p] Paper Msg [C-t] Title [C-a] Attach [C-s] Save [C-k] Clear [C-w] Close [C-u] Purge [C-x] Clear History [C-o] Sort"), "shortcutbar")
self.widget = urwid.AttrMap(urwid.Text("[C-d] Send [C-p] Paper Msg [C-t] Title [C-f] Attach [C-s] Save [Tab] ↑ Messages"), "shortcutbar")
class ConversationBodyShortcuts():
def __init__(self, app):
self.app = app
self.widget = urwid.AttrMap(urwid.Text("[C-s] Save [C-u] Purge [C-o] Sort [C-x] Clear History [C-g] Fullscreen [C-w] Close [Tab] ↓ Editor"), "shortcutbar")
class TabButton(urwid.Button):
button_left = urwid.Text("[")
@@ -223,6 +230,7 @@ class ConversationsDisplay():
self.list_shortcuts = ConversationListDisplayShortcuts(self.app)
self.editor_shortcuts = ConversationDisplayShortcuts(self.app)
self.body_shortcuts = ConversationBodyShortcuts(self.app)
self.shortcuts_display = self.list_shortcuts
self.widget = urwid.WidgetPlaceholder(self.columns_widget)
@@ -834,10 +842,10 @@ class ConversationsDisplay():
if display_name is None:
display_name = ""
e_id = urwid.Edit(caption="Addr : ",edit_text=source_hash_text)
e_id = ReadlineEdit(caption="Addr : ",edit_text=source_hash_text)
t_id = urwid.Text("Addr : "+source_hash_text)
e_name = urwid.Edit(caption="Name : ",edit_text=display_name)
e_copy = urwid.Edit(caption="Copy : ", edit_text=source_hash_text)
e_name = ReadlineEdit(caption="Name : ",edit_text=display_name)
e_copy = ReadlineEdit(caption="Copy : ", edit_text=source_hash_text)
selected_id_widget = t_id
@@ -878,7 +886,7 @@ class ConversationsDisplay():
except Exception as e:
pass
e_notes = urwid.Edit(caption="Notes: ", edit_text=notes_initial, multiline=True)
e_notes = ReadlineEdit(caption="Notes: ", edit_text=notes_initial, multiline=True)
cb_pin = urwid.CheckBox("Pin to top", state=pinned_initial)
trust_button_group = []
@@ -1018,8 +1026,8 @@ class ConversationsDisplay():
source_hash = ""
display_name = ""
e_id = urwid.Edit(caption="Addr : ",edit_text=source_hash)
e_name = urwid.Edit(caption="Name : ",edit_text=display_name)
e_id = ReadlineEdit(caption="Addr : ",edit_text=source_hash)
e_name = ReadlineEdit(caption="Name : ",edit_text=display_name)
trust_button_group = []
r_untrusted = urwid.RadioButton(trust_button_group, "Untrusted")
@@ -1110,7 +1118,7 @@ class ConversationsDisplay():
def ingest_lxm_uri(self):
self.dialog_open = True
lxm_uri = ""
e_uri = urwid.Edit(caption="URI : ",edit_text=lxm_uri)
e_uri = ReadlineEdit(caption="URI : ",edit_text=lxm_uri)
def dismiss_dialog(sender):
self.dialog_open = False
@@ -1410,7 +1418,7 @@ class ConversationsDisplay():
def show_set_pn_dialog(_sender):
current_pn = self.app.get_user_selected_propagation_node()
current_str = RNS.hexrep(current_pn, delimit=False) if current_pn is not None else ""
pn_edit = urwid.Edit(caption="Hash : ", edit_text=current_str)
pn_edit = ReadlineEdit(caption="Hash : ", edit_text=current_str)
status_text = urwid.Text("", align=urwid.CENTER)
def reopen_sync(_b=None):
@@ -1759,14 +1767,18 @@ class ConversationsDisplay():
focus_path = self.columns_widget.get_focus_path()
except Exception:
return self.list_shortcuts
if not focus_path:
if not focus_path or focus_path[0] != 1:
return self.list_shortcuts
if focus_path[0] == 0:
return self.list_shortcuts
elif focus_path[0] == 1:
try:
cw = self.columns_widget.contents[1][0]
frame = cw.base_widget.frame
if frame is None:
return self.editor_shortcuts
if frame.focus_position == "footer":
return self.editor_shortcuts
return self.body_shortcuts
except Exception:
return self.editor_shortcuts
else:
return self.list_shortcuts
class ListEntry(urwid.Text):
_selectable = True
@@ -1792,18 +1804,16 @@ class ListEntry(urwid.Text):
self._emit('click')
return True
class MessageEdit(urwid.Edit):
class MessageEdit(ReadlineMixin, urwid.Edit):
def keypress(self, size, key):
if key == "ctrl d":
self.delegate.send_message()
elif key == "ctrl p":
self.delegate.paper_message()
elif key == "ctrl a":
elif key == "ctrl f":
self.delegate.attach_file()
elif key == "ctrl s":
self.delegate.save_focused_attachments()
elif key == "ctrl k":
self.delegate.clear_editor()
elif key == "up":
y = self.get_cursor_coords(size)[1]
if y == 0:
@@ -1820,6 +1830,18 @@ class MessageEdit(urwid.Edit):
class ConversationFrame(urwid.Frame):
@property
def focus_position(self):
return urwid.Frame.focus_position.fget(self)
@focus_position.setter
def focus_position(self, part):
urwid.Frame.focus_position.fset(self, part)
try:
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.update_active_shortcuts()
except Exception:
pass
def keypress(self, size, key):
if self.focus_position == "header":
result = super(ConversationFrame, self).keypress(size, key)
@@ -1846,8 +1868,6 @@ class ConversationFrame(urwid.Frame):
self.focus_position = "footer"
else:
return super(ConversationFrame, self).keypress(size, key)
elif key == "ctrl k":
self.delegate.clear_editor()
else:
return super(ConversationFrame, self).keypress(size, key)
@@ -2198,7 +2218,11 @@ class ConversationWidget(urwid.WidgetWrap):
def keypress(self, size, key):
if key == "tab":
self.toggle_focus_area()
elif key == "ctrl w":
return None
key = super(ConversationWidget, self).keypress(size, key)
if key is None:
return None
if key == "ctrl w":
self.close()
elif key == "ctrl u":
self.conversation.purge_failed()
@@ -2217,7 +2241,7 @@ class ConversationWidget(urwid.WidgetWrap):
elif key == "ctrl s":
self.save_focused_attachments()
else:
return super(ConversationWidget, self).keypress(size, key)
return key
def _on_conversation_changed_from_callback(self, conversation):
self.delegate._wake(lambda: self.conversation_changed(conversation))
+75 -23
View File
@@ -190,7 +190,7 @@ class TopicList(urwid.WidgetWrap):
def keypress(self, size, key):
if key == "up" and (self.ilb.first_item_is_selected()):
nomadnet.NomadNetworkApp.get_shared_instance().ui.main_display.frame.focus_position = "header"
return super(TopicList, self).keypress(size, key)
class GuideDisplay():
@@ -307,16 +307,68 @@ The different sections of the program has a number of keyboard shortcuts mapped,
- Ctrl-E Display and edit selected peer info
- Ctrl-X Delete conversation
- Ctrl-R Open LXMF syncronisation dialog
>>>Conversation Display
- Ctrl-D Send message
- Ctrl-K Clear input fields
- Ctrl-T Toggle message title field
- Ctrl-U Ingest LXMF URI
- Ctrl-O Toggle sort mode
- Ctrl-P Display own LXMF address
- Ctrl-G Toggle fullscreen
>>>Message Editor
- Ctrl-D Send message
- Ctrl-P Compose paper message
- Ctrl-T Toggle message title field
- Ctrl-F Attach file
- Ctrl-S Save focused attachment
- Tab Switch focus to message list
>>>Message List
- Ctrl-W Close conversation
- Ctrl-U Purge failed messages
- Ctrl-O Toggle sort mode
- Ctrl-P Purge failed messages
- Ctrl-X Clear conversation history
- Ctrl-G Toggle fullscreen conversation
- Ctrl-W Close conversation
- Ctrl-S Save focused attachment
- Tab Switch focus to message editor
>>`!Channels Window`!
>>>Channel List
- Ctrl-N Add a new hub
- Ctrl-A Add (join) a room
- Ctrl-R Connect to selected hub
- Ctrl-W Disconnect from selected hub
- Ctrl-T Toggle auto-reconnect for selected hub
- Ctrl-E Edit selected hub
- Ctrl-X Remove selected hub
- F8 Toggle join/part collapse
>>>Message Editor
- Ctrl-D Send message
- Ctrl-X Leave current room
- F8 Toggle join/part collapse
- Tab Complete nickname
>>>Message List
- Ctrl-X Leave current room
- Ctrl-U Toggle users pane
- Ctrl-Y Toggle channel list
- F8 Toggle join/part collapse
- Tab Switch focus to message editor
>>`!Input Field Editing`!
All text input fields support readline-style editing shortcuts. When an input
field is focused, these take precedence over any window shortcut mapped to the
same key (so, for example, Ctrl-U edits the line rather than toggling a pane):
- Ctrl-A Move to beginning of line
- Ctrl-E Move to end of line
- Ctrl-U Delete from cursor to beginning of line
- Ctrl-K Delete from cursor to end of line
- Ctrl-W Delete previous word (whitespace-delimited)
- Ctrl-L Delete the entire buffer
- Ctrl-Y Paste (yank) most recently deleted text
- Ctrl-Left Move backward one word
- Ctrl-Right Move forward one word
Text deleted with Ctrl-U, Ctrl-K, Ctrl-W or Ctrl-L is placed in a shared yank
buffer that Ctrl-Y pastes back, so text can be moved between input fields.
>>`!Network Window`!
>>>Browser
@@ -491,29 +543,29 @@ Links can be inserted into micron documents. See the `*Markup`* section of this
TOPIC_INTERFACES = '''>Interfaces
Reticulum supports using many kinds of devices as networking interfaces, and allows you to mix and match them in any way you choose.
Reticulum supports using many kinds of devices as networking interfaces, and allows you to mix and match them in any way you choose.
The number of distinct network topologies you can create with Reticulum is more or less endless, but common to them all is that you will need to define one or more interfaces for Reticulum to use.
The `![ Interfaces ]`! section of NomadNet lets you add, monitor, and update interfaces configured for your Reticulum instance.
If you are starting NomadNet for the first time you will find that an `!AutoInterface`! has been added by default. This interface will try to use your available network device to communicate with other peers discovered on your local network.
If you are starting NomadNet for the first time you will find that an `!AutoInterface`! has been added by default. This interface will try to use your available network device to communicate with other peers discovered on your local network.
Interfaces come in many different types and can interact with physical mediums like LoRa radios or standard IP networks.
Interfaces come in many different types and can interact with physical mediums like LoRa radios or standard IP networks.
>>Viewing Interfaces
To view more info about an interface, navigate using the `!Up`! and `!Down`! arrow keys or by clicking with the mouse. Pressing `! < Enter >`! on a selected interface will bring you to a detailed interface view, which will show configuration parameters and realtime charts. From here you can also disable or edit the interface. To change the orientation of the TX/RX charts, press `!< V >`! for a vertical layout, and `!< H >`! for a horizontal view.
To view more info about an interface, navigate using the `!Up`! and `!Down`! arrow keys or by clicking with the mouse. Pressing `! < Enter >`! on a selected interface will bring you to a detailed interface view, which will show configuration parameters and realtime charts. From here you can also disable or edit the interface. To change the orientation of the TX/RX charts, press `!< V >`! for a vertical layout, and `!< H >`! for a horizontal view.
>>Updating Interfaces
To edit an interface, select the interface and press `!< Ctrl + E >`!.
To edit an interface, select the interface and press `!< Ctrl + E >`!.
To remove an interface, select the interface and press `!< Ctrl + X >`!. You can also perform both of these actions from the details view.
>>Adding Interfaces
To add a new interface, press `!< Ctrl + A >`!. From here you can select which type of interface you want to add. Each unique interface type will have different configuration options.
To add a new interface, press `!< Ctrl + A >`!. From here you can select which type of interface you want to add. Each unique interface type will have different configuration options.
`Ffff`! (!) Note:`! After adding or modifying interfaces, you will need to restart NomadNet or your Reticulum instance for changes to take effect.`f`b
@@ -548,14 +600,14 @@ Target Port: Port number to connect to
Optional Parameters:
I2P Tunneled: Enable for connecting through I2P
KISS Framing: Enable for KISS framing for software modems
KISS Framing: Enable for KISS framing for software modems
```
This interface is commonly used to connect to Reticulum gateways or other persistent nodes on the Internet.
>>TCPServerInterface
The TCP Server interface listens for incoming connections, allowing other Reticulum peers to connect to your node using TCPClientInterface.
The TCP Server interface listens for incoming connections, allowing other Reticulum peers to connect to your node using TCPClientInterface.
```
Required Parameters:
@@ -603,7 +655,7 @@ The RNode interface allows using LoRa transceivers running RNode firmware as Ret
Required Parameters:
Port: Serial port or BLE device path
Frequency: Operating frequency in MHz
Bandwidth: Channel bandwidth
Bandwidth: Channel bandwidth
TX Power: Transmit power in dBm
Spreading Factor: LoRa spreading factor (7-12)
Coding Rate: LoRa coding rate (4:5-4:8)
@@ -614,7 +666,7 @@ ID Interval: Identification interval in seconds
Airtime Limits: Control duty cycle
```
The interface includes a parameter calculator to estimate link budget, sensitivity, and data rate on the air based on your settings.
The interface includes a parameter calculator to estimate link budget, sensitivity, and data rate on the air based on your settings.
>>RNodeMultiInterface
@@ -648,7 +700,7 @@ The KISS interface supports packet radio modems and TNCs using the KISS protocol
Required Parameters:
Port: Serial port path
Speed: Baud rate of serial device
Speed: Baud rate of serial device
Databits: Number of data bits
Parity: Parity setting
Stopbits: Number of stop bits
@@ -1445,7 +1497,7 @@ If no heading text is defined, the section will appear as a sub-section without
Tags are used to format text with micron. Some tags can appear anywhere in text, and some must appear at the beginning of a line. If you need to write text that contains a sequence that would be interpreted as a tag, you can escape it with the character \\.
In the following sections, the different tags will be introduced. Any styling set within micron can be reset to the default style by using the special \\`\\` tag anywhere in the markup, which will immediately remove any formatting previously specified.
In the following sections, the different tags will be introduced. Any styling set within micron can be reset to the default style by using the special \\`\\` tag anywhere in the markup, which will immediately remove any formatting previously specified.
>>Alignment
@@ -1646,7 +1698,7 @@ If you want to link to an anchor on another page, you can include it as a reques
>>Notes on namespaces and collisions
Auto-anchors from headings and explicit \\`: anchors share a single namespace per page. If an explicit anchor collides with a heading slug, the first one declared is where it will jump to.
Auto-anchors from headings and explicit \\`: anchors share a single namespace per page. If an explicit anchor collides with a heading slug, the first one declared is where it will jump to.
>Tables
@@ -1761,7 +1813,7 @@ Full control: `B444`<!32|all_options`hidden text>`B333
>>> Checkboxes
In addition to text fields, Checkboxes are another way of submitting data. They allow the user to make a single selection or select multiple options.
In addition to text fields, Checkboxes are another way of submitting data. They allow the user to make a single selection or select multiple options.
`Faaa
`=
@@ -1827,7 +1879,7 @@ This line will
>Partials
You can include partials in pages, which will load asynchronously once the page itself has loaded.
You can include partials in pages, which will load asynchronously once the page itself has loaded.
`Faaa
`=
+3 -2
View File
@@ -6,6 +6,7 @@ from math import log10, pow
from nomadnet.vendor.additional_urwid_widgets.FormWidgets import *
from nomadnet.vendor.AsciiChart import AsciiChart
from .ReadlineEdit import ReadlineEdit
### GYLPHS ###
INTERFACE_GLYPHS = {
@@ -235,8 +236,8 @@ class RNodeCalculator(urwid.WidgetWrap):
self.link_budget_widget = urwid.Text("Link Budget: Calculating...")
self.sensitivity_widget = urwid.Text("Sensitivity: Calculating...")
self.noise_floor_edit = urwid.Edit("", "0")
self.antenna_gain_edit = urwid.Edit("", "0")
self.noise_floor_edit = ReadlineEdit("", "0")
self.antenna_gain_edit = ReadlineEdit("", "0")
layout = urwid.Pile([
urwid.Divider("-"),
+2 -1
View File
@@ -6,6 +6,7 @@ import time
import RNS
from urwid.util import is_mouse_press
from urwid.text_layout import calc_coords
from .ReadlineEdit import ReadlineEdit
from RNS.Utilities.rngit.util import MarkdownToMicron
DEFAULT_FG_DARK = "ddd"
@@ -360,7 +361,7 @@ def parse_line(line, state, url_delegate):
fn = o["name"]
fs = o["style"]
fmask = "*" if o["masked"] else None
f = urwid.Edit(caption="", edit_text=fd, align=state["align"], multiline=True, mask=fmask)
f = ReadlineEdit(caption="", edit_text=fd, align=state["align"], multiline=True, mask=fmask)
f.field_name = fn
fa = urwid.AttrMap(f, fs)
widgets.append((fw, fa))
+5 -4
View File
@@ -10,6 +10,7 @@ from nomadnet.util import strip_modifiers
from nomadnet.util import sanitize_name
from .Browser import Browser
from .ReadlineEdit import ReadlineEdit
class NetworkDisplayShortcuts():
def __init__(self, app):
@@ -415,7 +416,7 @@ class AnnounceStream(urwid.WidgetWrap):
('weight', 3, self.tab_pn),
], dividechars=1)
self.search_edit = urwid.Edit(caption="Search: ")
self.search_edit = ReadlineEdit(caption="Search: ")
urwid.connect_signal(self.search_edit, 'change', self.on_search_change)
self.display_toggle = TabButton("Show: Name", on_press=self.toggle_display_mode)
@@ -674,8 +675,8 @@ class KnownNodeInfo(urwid.WidgetWrap):
r_unknown = urwid.RadioButton(trust_button_group, "Unknown", state=unknown_selected)
r_trusted = urwid.RadioButton(trust_button_group, "Trusted", state=trusted_selected)
e_name = urwid.Edit(caption="Name : ",edit_text=display_str)
e_sort = urwid.Edit(caption="Sort Rank : ",edit_text=sort_str)
e_name = ReadlineEdit(caption="Name : ",edit_text=display_str)
e_sort = ReadlineEdit(caption="Sort Rank : ",edit_text=sort_str)
node_ident = RNS.Identity.recall(source_hash)
op_hash = None
@@ -1269,7 +1270,7 @@ class LocalPeer(urwid.WidgetWrap):
t_id = urwid.Text("LXMF Addr : "+RNS.prettyhexrep(self.app.lxmf_destination.hash))
i_id = urwid.Text("Identity : "+RNS.prettyhexrep(self.app.identity.hash))
e_name = urwid.Edit(caption="Name : ", edit_text=display_name)
e_name = ReadlineEdit(caption="Name : ", edit_text=display_name)
def save_query(sender):
def dismiss_dialog(sender):
+150
View File
@@ -0,0 +1,150 @@
import urwid
_KILL_KEYS = frozenset(("ctrl u", "ctrl k", "ctrl w", "ctrl l"))
class _KillRing:
"""Module-global kill buffer shared across all ReadlineMixin widgets.
Mirrors GNU readline: consecutive kills accumulate into the same entry
(forward kills append, backward kills prepend); any non-kill keypress
breaks the chain so the next kill replaces the buffer.
"""
text = ""
last_was_kill = False
@classmethod
def reset_chain(cls):
cls.last_was_kill = False
@classmethod
def kill(cls, killed, direction):
if not killed:
return
if cls.last_was_kill:
cls.text = cls.text + killed if direction == "forward" else killed + cls.text
else:
cls.text = killed
cls.last_was_kill = True
class ReadlineMixin:
"""Mixin adding readline-style editing keys to an urwid.Edit-derived widget.
Bindings (GNU readline defaults, plus ctrl-l as kill-whole-buffer):
ctrl-a beginning-of-line
ctrl-e end-of-line
ctrl-u unix-line-discard (kill from cursor to beginning of line)
ctrl-k kill-line (kill from cursor to end of line)
ctrl-w unix-word-rubout (kill previous whitespace-delimited word)
ctrl-l kill-whole-buffer (kill the entire edit buffer)
ctrl-y yank (insert most-recently-killed text)
ctrl-left backward-word (alphanumeric boundary)
ctrl-right forward-word (alphanumeric boundary)
"Line" is the current logical line within the edit buffer, delimited by
newlines -- so on multiline Edits these act on the line under the cursor,
not the entire buffer.
The kill buffer is shared across all widgets using this mixin, so text
killed in one Edit can be yanked into another.
"""
def keypress(self, size, key):
if key == "ctrl a": self._rl_beg_of_line()
elif key == "ctrl e": self._rl_end_of_line()
elif key == "ctrl u": self._rl_kill_to_beg()
elif key == "ctrl k": self._rl_kill_to_end()
elif key == "ctrl w": self._rl_kill_word_back()
elif key == "ctrl l": self._rl_kill_whole_buffer()
elif key == "ctrl y": self._rl_yank()
elif key == "ctrl left": self._rl_backward_word()
elif key == "ctrl right": self._rl_forward_word()
else:
result = super().keypress(size, key)
if key not in _KILL_KEYS:
_KillRing.reset_chain()
return result
if key not in _KILL_KEYS:
_KillRing.reset_chain()
return None
def _rl_line_bounds(self):
text, pos = self.edit_text, self.edit_pos
bol = text.rfind("\n", 0, pos)
bol = 0 if bol == -1 else bol + 1
eol = text.find("\n", pos)
eol = len(text) if eol == -1 else eol
return bol, eol
def _rl_delete(self, start, end, kill_direction=None):
if start == end:
return
text = self.edit_text
if kill_direction is not None:
_KillRing.kill(text[start:end], kill_direction)
self.set_edit_text(text[:start] + text[end:])
self.set_edit_pos(start)
@staticmethod
def _rl_is_word_char(ch):
return ch.isalnum() or ch == "_"
def _rl_beg_of_line(self):
bol, _ = self._rl_line_bounds()
self.set_edit_pos(bol)
def _rl_end_of_line(self):
_, eol = self._rl_line_bounds()
self.set_edit_pos(eol)
def _rl_kill_to_beg(self):
bol, _ = self._rl_line_bounds()
self._rl_delete(bol, self.edit_pos, kill_direction="backward")
def _rl_kill_to_end(self):
_, eol = self._rl_line_bounds()
self._rl_delete(self.edit_pos, eol, kill_direction="forward")
def _rl_kill_word_back(self):
text, pos = self.edit_text, self.edit_pos
p = pos
while p > 0 and text[p - 1].isspace():
p -= 1
while p > 0 and not text[p - 1].isspace():
p -= 1
self._rl_delete(p, pos, kill_direction="backward")
def _rl_kill_whole_buffer(self):
self._rl_delete(0, len(self.edit_text), kill_direction="forward")
def _rl_yank(self):
if not _KillRing.text:
return
pos = self.edit_pos
text = self.edit_text
self.set_edit_text(text[:pos] + _KillRing.text + text[pos:])
self.set_edit_pos(pos + len(_KillRing.text))
def _rl_backward_word(self):
text, pos = self.edit_text, self.edit_pos
while pos > 0 and not self._rl_is_word_char(text[pos - 1]):
pos -= 1
while pos > 0 and self._rl_is_word_char(text[pos - 1]):
pos -= 1
self.set_edit_pos(pos)
def _rl_forward_word(self):
text, pos = self.edit_text, self.edit_pos
n = len(text)
while pos < n and not self._rl_is_word_char(text[pos]):
pos += 1
while pos < n and self._rl_is_word_char(text[pos]):
pos += 1
self.set_edit_pos(pos)
class ReadlineEdit(ReadlineMixin, urwid.Edit):
"""Drop-in urwid.Edit replacement with readline-style editing keys."""
pass
+9 -7
View File
@@ -1,5 +1,7 @@
import urwid
from nomadnet.ui.textui.ReadlineEdit import ReadlineMixin, ReadlineEdit
class DialogLineBox(urwid.LineBox):
def __init__(self, body, parent=None, title="?"):
super().__init__(body, title=title)
@@ -12,7 +14,7 @@ class DialogLineBox(urwid.LineBox):
return None
return super().keypress(size, key)
class Placeholder(urwid.Edit):
class Placeholder(ReadlineMixin, urwid.Edit):
def __init__(self, caption="", edit_text="", placeholder="", **kwargs):
super().__init__(caption, edit_text, **kwargs)
self.placeholder = placeholder
@@ -240,7 +242,7 @@ class FormMultiList(urwid.Pile, FormField):
FormField.__init__(self, config_key, transform)
def create_entry_row(self):
edit = urwid.Edit("", "")
edit = ReadlineEdit("", "")
entry_row = urwid.Columns([
('weight', 1, edit),
(3, urwid.Button("×", on_press=lambda button: self.remove_entry(button, entry_row))),
@@ -318,7 +320,7 @@ class FormMultiTable(urwid.Pile, FormField):
if values is None:
values = {}
name_edit = urwid.Edit("", name)
name_edit = ReadlineEdit("", name)
columns = [('weight', 3, name_edit)]
@@ -330,9 +332,9 @@ class FormMultiTable(urwid.Pile, FormField):
widget = urwid.CheckBox("", state=bool(field_value))
elif field_config.get("type") == "dropdown":
# TODO: dropdown in MultiTable
widget = urwid.Edit("", str(field_value))
widget = ReadlineEdit("", str(field_value))
else:
widget = urwid.Edit("", str(field_value))
widget = ReadlineEdit("", str(field_value))
field_widgets[field_key] = widget
columns.append(('weight', 2, widget))
@@ -451,8 +453,8 @@ class FormKeyValuePairs(urwid.Pile, FormField):
FormField.__init__(self, config_key, transform)
def create_entry_row(self, key="", value=""):
key_edit = urwid.Edit("", key)
value_edit = urwid.Edit("", value)
key_edit = ReadlineEdit("", key)
value_edit = ReadlineEdit("", value)
remove_button = urwid.Button("×", on_press=lambda button: self.remove_entry(button, entry_row))