Skip to content

Commit d6fea76

Browse files
committed
feat(command): Open external editor command in composing message.
OPEN_EXTERNAL_EDITOR command paste the composing message in a python tempfile, run external editor over it and wait the exit before update the message. ZULIPRC editor key, $ZULIP_EDITOR_COMMAND and fallback $EDITOR are use for the external editor command. Use shlex to split command.
1 parent d46b6d8 commit d6fea76

File tree

9 files changed

+95
-0
lines changed

9 files changed

+95
-0
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ notify=disabled
246246
247247
## Color-depth: set to one of 1 (for monochrome), 16, 256, or 24bit
248248
color-depth=256
249+
250+
## Editor: set external editor command, fallback to $ZULIP_EDITOR_COMMAND and $EDITOR env
251+
# editor: nano
249252
```
250253

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

docs/FAQ.md

+30
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [When are messages marked as having been read?](#when-are-messages-marked-as-having-been-read)
1414
- [How do I access multiple servers?](#how-do-i-access-multiple-servers)
1515
- [What is autocomplete? Why is it useful?](#what-is-autocomplete-why-is-it-useful)
16+
- [Can I compose messages in another editor?](#can-i-compose-messages-in-another-editor)
1617
- Something is not working!
1718
- [Colors appear mismatched, don't change with theme, or look strange](#colors-appear-mismatched-dont-change-with-theme-or-look-strange)
1819
- [Symbols look different to in the provided screenshots, or just look incorrect](#symbols-look-different-to-in-the-provided-screenshots-or-just-look-incorrect)
@@ -373,6 +374,35 @@ through autocomplete depend upon the context automatically.
373374
**NOTE:** If a direct message recipient's name contains comma(s) (`,`), they
374375
are currently treated as comma-separated recipients.
375376

377+
## Can I compose messages in another editor?
378+
379+
In the main branch of zulip-terminal, you can now use an external editor to
380+
compose your message using `ctrl o` shortcut. If `ZULIP_EDITOR_COMMAND` or
381+
`EDITOR` environment variable is set, this command or program would be used
382+
to open the message by appending a temporary file filepath of the current message.
383+
384+
It will work directly for most terminal editors with only the program name `vim`,
385+
`nano`, `helix`, `kakoune`, `nvim`...
386+
387+
It can also be used for desktop editor with some constraint which needs to be
388+
address using `ZULIP_EDITOR_COMMAND` custom command. The program must not fork
389+
or detach from the running terminal and should open in a new window, some
390+
examples:
391+
392+
- [lapce](https://github.com/lapce/lapce) with `lapce -n -w`
393+
- [sublime-text](https://www.sublimetext.com/) with `subl -n -w`
394+
- [marker](https://github.com/fabiocolacio/Marker) with `marker`
395+
- [vim](https://github.com/vim/vim) with `vim -g -f` or `gvim -f`
396+
- [vscode](https://github.com/microsoft/vscode) with `code -n -w`
397+
398+
When the external editor process ends (closing the window or quitting terminal
399+
editor), the composing box will be updated with the new message content from
400+
the temporary file.
401+
402+
**NOTE:** Backslashing white space (`\ `) is needed when using an executable
403+
containing them, for example for Sublime Text on macOS can be configure with
404+
`/Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl`.
405+
376406
## Colors appear mismatched, don't change with theme, or look strange
377407

378408
Some terminal emulators support specifying custom colors, or custom color

docs/hotkeys.md

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
|Cycle through autocomplete suggestions in reverse|<kbd>Ctrl</kbd> + <kbd>r</kbd>|
8888
|Narrow to compose box message recipient|<kbd>Meta</kbd> + <kbd>.</kbd>|
8989
|Exit message compose box|<kbd>Esc</kbd>|
90+
|Open the message in external editor|<kbd>Ctrl</kbd> + <kbd>o</kbd>|
9091
|Insert new line|<kbd>Enter</kbd>|
9192

9293
## Editor: Navigation

tests/cli/test_run.py

+5
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",
@@ -203,6 +205,7 @@ def test_valid_zuliprc_but_no_connection(
203205
" maximum footlinks value '3' specified from default config.",
204206
" color depth setting '256' specified from default config.",
205207
" notify setting 'disabled' specified from default config.",
208+
" external editor command '' specified from environment.",
206209
"\x1b[91m",
207210
f"Error connecting to Zulip server: {server_connection_error}.\x1b[0m",
208211
]
@@ -262,6 +265,7 @@ def test_warning_regarding_incomplete_theme(
262265
" maximum footlinks value '3' specified from default config.",
263266
" color depth setting '256' specified from default config.",
264267
" notify setting 'disabled' specified from default config.",
268+
" external editor command '' specified from environment.",
265269
"\x1b[91m",
266270
f"Error connecting to Zulip server: {server_connection_error}.\x1b[0m",
267271
]
@@ -481,6 +485,7 @@ def test_successful_main_function_with_config(
481485
f" maximum footlinks value {footlinks_output}",
482486
" color depth setting '256' specified in zuliprc file.",
483487
" notify setting 'enabled' specified in zuliprc file.",
488+
" external editor command '' specified from environment.",
484489
]
485490
assert lines == expected_lines
486491

tests/core/test_core.py

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def controller(self, mocker: MockerFixture) -> Controller:
5858
color_depth=256,
5959
in_explore_mode=self.in_explore_mode,
6060
debug_path=None,
61+
editor_command="",
6162
**dict(
6263
autohide=self.autohide,
6364
notify=self.notify_enabled,

zulipterminal/cli/run.py

+17
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

@@ -80,6 +81,7 @@ class SettingData(NamedTuple):
8081
"color-depth": "256",
8182
"maximum-footlinks": "3",
8283
"exit_confirmation": "enabled",
84+
"editor": "",
8385
}
8486
assert DEFAULT_SETTINGS["autohide"] in VALID_BOOLEAN_SETTINGS["autohide"]
8587
assert DEFAULT_SETTINGS["notify"] in VALID_BOOLEAN_SETTINGS["notify"]
@@ -553,6 +555,20 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None:
553555
print_setting("maximum footlinks value", zterm["maximum-footlinks"])
554556
print_setting("color depth setting", zterm["color-depth"])
555557
print_setting("notify setting", zterm["notify"])
558+
if zterm["editor"].source == ConfigSource.ZULIPRC:
559+
editor_command = zterm["editor"].value
560+
editor_config_source = ConfigSource.ZULIPRC
561+
else:
562+
editor_command = os.environ.get(
563+
"ZULIP_EDITOR_COMMAND",
564+
os.environ.get("EDITOR", ""),
565+
)
566+
editor_config_source = ConfigSource.ENV
567+
568+
print_setting(
569+
"external editor command",
570+
SettingData(editor_command, editor_config_source),
571+
)
556572

557573
### Generate data not output to user, but into Controller
558574
# Generate urwid palette
@@ -575,6 +591,7 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None:
575591
in_explore_mode=args.explore,
576592
**boolean_settings,
577593
debug_path=debug_path,
594+
editor_command=editor_command,
578595
).main()
579596
except ServerConnectionFailure as e:
580597
# Acts as separator between logs

zulipterminal/config/keys.py

+5
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,11 @@ class KeyBinding(TypedDict):
340340
'help_text': 'Show/hide user information (from users list)',
341341
'key_category': 'general',
342342
},
343+
'OPEN_EXTERNAL_EDITOR': {
344+
'keys': ['ctrl o'],
345+
'help_text': 'Open the message in external editor',
346+
'key_category': 'msg_compose',
347+
},
343348
'BEGINNING_OF_LINE': {
344349
'keys': ['ctrl a', 'home'],
345350
'help_text': 'Start of line',

zulipterminal/core.py

+2
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
autohide: bool,
7475
notify: bool,
@@ -82,6 +83,7 @@ def __init__(
8283
self.exit_confirmation = exit_confirmation
8384
self.notify_enabled = notify
8485
self.maximum_footlinks = maximum_footlinks
86+
self.editor_command = editor_command
8587

8688
self.debug_path = debug_path
8789

zulipterminal/ui_tools/boxes.py

+31
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

@@ -807,6 +811,33 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
807811
elif is_command_key("MARKDOWN_HELP", key):
808812
self.view.controller.show_markdown_help()
809813
return key
814+
elif is_command_key("OPEN_EXTERNAL_EDITOR", key):
815+
editor = self.view.controller.editor_command
816+
if editor == "":
817+
self.view.controller.report_error(
818+
"Configure zuliprc file editor key, $EDITOR or "
819+
"$ZULIP_EDITOR_COMMAND shell environment."
820+
)
821+
return key
822+
editor_splits = shlex.split(editor)
823+
fullpath_program = shutil.which(editor_splits[0])
824+
if fullpath_program is None:
825+
self.view.controller.report_error(
826+
"Editor program not found, check $EDITOR "
827+
"or $ZULIP_EDITOR_COMMAND."
828+
)
829+
return key
830+
editor_splits[0] = fullpath_program
831+
with NamedTemporaryFile(suffix=".md") as edit_tempfile:
832+
with open(edit_tempfile.name, mode="w") as edit_writer:
833+
edit_writer.write(self.msg_write_box.edit_text)
834+
self.view.controller.loop.screen.stop()
835+
editor_splits.append(edit_tempfile.name)
836+
subprocess.call(editor_splits)
837+
with open(edit_tempfile.name, mode="r") as edit_reader:
838+
self.msg_write_box.edit_text = edit_reader.read().rstrip()
839+
self.view.controller.loop.screen.start()
840+
return key
810841
elif is_command_key("SAVE_AS_DRAFT", key):
811842
if self.msg_edit_state is None:
812843
if self.compose_box_status == "open_with_private":

0 commit comments

Comments
 (0)