Skip to content

Commit a8edd7b

Browse files
mek-ytneiljp
authored andcommitted
run/keys/core/boxes: Allow editing message content via external editors.
A new OPEN_EXTERNAL_EDITOR command (hotkey: Ctrl o) may now open an external editor, if details of one are provided, to allow message content to be edited outside of the application, during message compose or editing. The external editor may be specified by, in order of increasing precedence: - the semi-standard EDITOR environment variable - a ZULIP_EDIT_COMMAND environment variable - a new 'editor' key in the zuliprc file ZULIP_EDIT_COMMAND is added to allow a custom form of EDITOR to be used, ie. to avoid causing issues with other tools that may depend upon the latter. The command is validated as being set and the path to it as existing, with errors reported to the user. The feature then operates via a randomly-named temporary file, initialized with any existing message content, which is opened by the application with the editor. The application pauses at this point, until the editing is complete, when the content is substituted back into the compose content area, and the application resumes. README updated to document new zuliprc option. hotkeys automaically updated for new command. Tests updated. Additional defensive approaches and textual changes made by neiljp.
1 parent deaf55a commit a8edd7b

File tree

8 files changed

+81
-0
lines changed

8 files changed

+81
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ color-depth=256
251251
## This is highly dependent on a suitable terminal emulator, and support in the selected theme
252252
## Terminal emulators without this feature may show an arbitrary solid background color
253253
transparency=disabled
254+
255+
## Editor: set external editor command, to edit message content
256+
## If not set, this falls back to the $ZULIP_EDITOR_COMMAND then $EDITOR environment variables
257+
# editor: nano
254258
```
255259

256260
> **NOTE:** Most of these configuration settings may be specified on the

docs/hotkeys.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
|Cycle through autocomplete suggestions in reverse|<kbd>Ctrl</kbd> + <kbd>r</kbd>|
9898
|Exit message compose box|<kbd>Esc</kbd>|
9999
|Insert new line|<kbd>Enter</kbd>|
100+
|Open an external editor to edit the message content|<kbd>Ctrl</kbd> + <kbd>o</kbd>|
100101

101102
## Editor: Navigation
102103
|Command|Key Combination|

tests/cli/test_run.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
MODULE = "zulipterminal.cli.run"
2828
CONTROLLER = MODULE + ".Controller"
2929

30+
os.environ["ZULIP_EDITOR_COMMAND"] = ""
31+
3032

3133
@pytest.mark.parametrize(
3234
"color, code",
@@ -206,6 +208,7 @@ def test_valid_zuliprc_but_no_connection(
206208
" color depth setting '256' specified from default config.",
207209
" notify setting 'disabled' specified from default config.",
208210
" transparency setting 'disabled' specified from default config.",
211+
" external editor command '' specified from environment.",
209212
"\x1b[91m",
210213
f"Error connecting to Zulip server: {server_connection_error}.\x1b[0m",
211214
]
@@ -266,6 +269,7 @@ def test_warning_regarding_incomplete_theme(
266269
" color depth setting '256' specified from default config.",
267270
" notify setting 'disabled' specified from default config.",
268271
" transparency setting 'disabled' specified from default config.",
272+
" external editor command '' specified from environment.",
269273
"\x1b[91m",
270274
f"Error connecting to Zulip server: {server_connection_error}.\x1b[0m",
271275
]
@@ -486,6 +490,7 @@ def test_successful_main_function_with_config(
486490
" color depth setting '256' specified in zuliprc file.",
487491
" notify setting 'enabled' specified in zuliprc file.",
488492
" transparency setting 'disabled' specified from default config.",
493+
" external editor command '' specified from environment.",
489494
]
490495
assert lines == expected_lines
491496

tests/core/test_core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def controller(self, mocker: MockerFixture) -> Controller:
6161
color_depth=256,
6262
in_explore_mode=self.in_explore_mode,
6363
debug_path=None,
64+
editor_command="",
6465
**dict(
6566
autohide=self.autohide,
6667
notify=self.notify_enabled,

zulipterminal/cli/run.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
class ConfigSource(Enum):
3434
DEFAULT = "from default config"
3535
ZULIPRC = "in zuliprc file"
36+
ENV = "from environment"
3637
COMMANDLINE = "on command line"
3738

3839

@@ -82,6 +83,7 @@ class SettingData(NamedTuple):
8283
"maximum-footlinks": "3",
8384
"exit_confirmation": "enabled",
8485
"transparency": "disabled",
86+
"editor": "",
8587
}
8688
assert DEFAULT_SETTINGS["autohide"] in VALID_BOOLEAN_SETTINGS["autohide"]
8789
assert DEFAULT_SETTINGS["notify"] in VALID_BOOLEAN_SETTINGS["notify"]
@@ -579,6 +581,21 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None:
579581
print_setting("notify setting", zterm["notify"])
580582
print_setting("transparency setting", zterm["transparency"])
581583

584+
if zterm["editor"].source == ConfigSource.ZULIPRC:
585+
editor_command = zterm["editor"].value
586+
editor_config_source = ConfigSource.ZULIPRC
587+
else:
588+
editor_command = os.environ.get(
589+
"ZULIP_EDITOR_COMMAND",
590+
os.environ.get("EDITOR", ""),
591+
)
592+
editor_config_source = ConfigSource.ENV
593+
594+
print_setting(
595+
"external editor command",
596+
SettingData(editor_command, editor_config_source),
597+
)
598+
582599
### Generate data not output to user, but into Controller
583600
# Translate valid strings for boolean values into True/False
584601
boolean_settings: Dict[str, bool] = dict()
@@ -605,6 +622,7 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None:
605622
in_explore_mode=args.explore,
606623
**boolean_settings,
607624
debug_path=debug_path,
625+
editor_command=editor_command,
608626
).main()
609627
except ServerConnectionFailure as e:
610628
# Acts as separator between logs

zulipterminal/config/keys.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,11 @@ class KeyBinding(TypedDict):
428428
'help_text': 'Insert new line',
429429
'key_category': 'compose_box',
430430
},
431+
'OPEN_EXTERNAL_EDITOR': {
432+
'keys': ['ctrl o'],
433+
'help_text': 'Open an external editor to edit the message content',
434+
'key_category': 'compose_box',
435+
},
431436
'FULL_RENDERED_MESSAGE': {
432437
'keys': ['f'],
433438
'help_text': 'Show/hide full rendered message (from message information)',

zulipterminal/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(
6969
theme: ThemeSpec,
7070
color_depth: int,
7171
debug_path: Optional[str],
72+
editor_command: str,
7273
in_explore_mode: bool,
7374
transparency: bool,
7475
autohide: bool,
@@ -84,6 +85,7 @@ def __init__(
8485
self.exit_confirmation = exit_confirmation
8586
self.notify_enabled = notify
8687
self.maximum_footlinks = maximum_footlinks
88+
self.editor_command = editor_command
8789

8890
self.debug_path = debug_path
8991

zulipterminal/ui_tools/boxes.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
"""
44

55
import re
6+
import shlex
7+
import shutil
8+
import subprocess
69
import unicodedata
710
from collections import Counter
811
from datetime import datetime, timedelta
12+
from tempfile import NamedTemporaryFile
913
from time import sleep
1014
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple
1115

@@ -839,6 +843,47 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
839843
elif is_command_key("MARKDOWN_HELP", key):
840844
self.view.controller.show_markdown_help()
841845
return key
846+
elif is_command_key("OPEN_EXTERNAL_EDITOR", key):
847+
editor_command = self.view.controller.editor_command
848+
849+
# None would indicate for shlex.split to read sys.stdin for Python < 3.12
850+
# It should never occur in practice
851+
assert isinstance(editor_command, str)
852+
853+
if editor_command == "":
854+
self.view.controller.report_error(
855+
"No external editor command specified; "
856+
"Set 'editor' in zuliprc file, or "
857+
"$ZULIP_EDITOR_COMMAND or $EDITOR environment variables."
858+
)
859+
return key
860+
861+
editor_command_line: List[str] = shlex.split(editor_command)
862+
if not editor_command_line:
863+
fullpath_program = None # A command may be specified, but empty
864+
else:
865+
fullpath_program = shutil.which(editor_command_line[0])
866+
if fullpath_program is None:
867+
self.view.controller.report_error(
868+
"External editor command not found; "
869+
"Check your zuliprc file, $EDITOR or $ZULIP_EDITOR_COMMAND."
870+
)
871+
return key
872+
editor_command_line[0] = fullpath_program
873+
874+
with NamedTemporaryFile(suffix=".md") as edit_tempfile:
875+
with open(edit_tempfile.name, mode="w") as edit_writer:
876+
edit_writer.write(self.msg_write_box.edit_text)
877+
self.view.controller.loop.screen.stop()
878+
879+
editor_command_line.append(edit_tempfile.name)
880+
subprocess.call(editor_command_line)
881+
882+
with open(edit_tempfile.name, mode="r") as edit_reader:
883+
self.msg_write_box.edit_text = edit_reader.read().rstrip()
884+
self.view.controller.loop.screen.start()
885+
return key
886+
842887
elif is_command_key("SAVE_AS_DRAFT", key):
843888
if self.msg_edit_state is None:
844889
if self.compose_box_status == "open_with_private":

0 commit comments

Comments
 (0)