Skip to content

Commit

Permalink
feat(command): open external editor command in composing message
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mek-yt committed Dec 28, 2023
1 parent 1bc8175 commit cc1b0e5
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ notify=disabled
## Color-depth: set to one of 1 (for monochrome), 16, 256, or 24bit
color-depth=256
## Editor: set external editor command, fallback to $ZULIP_EDITOR_COMMAND and $EDITOR env
# editor: nano
```

> **NOTE:** Most of these configuration settings may be specified on the
Expand Down
30 changes: 30 additions & 0 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [When are messages marked as having been read?](#when-are-messages-marked-as-having-been-read)
- [How do I access multiple servers?](#how-do-i-access-multiple-servers)
- [What is autocomplete? Why is it useful?](#what-is-autocomplete-why-is-it-useful)
- [Can I compose messages in another editor?](#can-i-compose-messages-in-another-editor)
- Something is not working!
- [Colors appear mismatched, don't change with theme, or look strange](#colors-appear-mismatched-dont-change-with-theme-or-look-strange)
- [Symbols look different to in the provided screenshots, or just look incorrect](#symbols-look-different-to-in-the-provided-screenshots-or-just-look-incorrect)
Expand Down Expand Up @@ -373,6 +374,35 @@ through autocomplete depend upon the context automatically.
**NOTE:** If a direct message recipient's name contains comma(s) (`,`), they
are currently treated as comma-separated recipients.

## Can I compose messages in another editor?

In the main branch of zulip-terminal, you can now use an external editor to
compose your message using `ctrl o` shortcut. If `ZULIP_EDITOR_COMMAND` or
`EDITOR` environment variable is set, this command or program would be used
to open the message by appending a temporary file filepath of the current message.

It will work directly for most terminal editors with only the program name `vim`,
`nano`, `helix`, `kakoune`, `nvim`...

It can also be used for desktop editor with some constraint which needs to be
address using `ZULIP_EDITOR_COMMAND` custom command. The program must not fork
or detach from the running terminal and should open in a new window, some
examples:

- [lapce](https://github.com/lapce/lapce) with `lapce -n -w`
- [sublime-text](https://www.sublimetext.com/) with `subl -n -w`
- [marker](https://github.com/fabiocolacio/Marker) with `marker`
- [vim](https://github.com/vim/vim) with `vim -g -f` or `gvim -f`
- [vscode](https://github.com/microsoft/vscode) with `code -n -w`

When the external editor process ends (closing the window or quitting terminal
editor), the composing box will be updated with the new message content from
the temporary file.

**NOTE:** Backslashing white space (`\ `) is needed when using an executable
containing them, for example for Sublime Text on macOS can be configure with
`/Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl`.

## Colors appear mismatched, don't change with theme, or look strange

Some terminal emulators support specifying custom colors, or custom color
Expand Down
1 change: 1 addition & 0 deletions docs/hotkeys.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
|Autocomplete @mentions, #stream_names, :emoji: and topics|<kbd>ctrl</kbd> + <kbd>f</kbd>|
|Cycle through autocomplete suggestions in reverse|<kbd>ctrl</kbd> + <kbd>r</kbd>|
|Narrow to compose box message recipient|<kbd>meta</kbd> + <kbd>.</kbd>|
|Open the message in external editor|<kbd>ctrl</kbd> + <kbd>o</kbd>|
|Jump to the beginning of line|<kbd>ctrl</kbd> + <kbd>a</kbd>|
|Jump to the end of line|<kbd>ctrl</kbd> + <kbd>e</kbd>|
|Jump backward one word|<kbd>meta</kbd> + <kbd>b</kbd>|
Expand Down
5 changes: 5 additions & 0 deletions tests/cli/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
MODULE = "zulipterminal.cli.run"
CONTROLLER = MODULE + ".Controller"

os.environ["ZULIP_EDITOR_COMMAND"] = ""


@pytest.mark.parametrize(
"color, code",
Expand Down Expand Up @@ -203,6 +205,7 @@ def test_valid_zuliprc_but_no_connection(
" maximum footlinks value '3' specified from default config.",
" color depth setting '256' specified from default config.",
" notify setting 'disabled' specified from default config.",
" external editor command '' specified from environment.",
"\x1b[91m",
f"Error connecting to Zulip server: {server_connection_error}.\x1b[0m",
]
Expand Down Expand Up @@ -262,6 +265,7 @@ def test_warning_regarding_incomplete_theme(
" maximum footlinks value '3' specified from default config.",
" color depth setting '256' specified from default config.",
" notify setting 'disabled' specified from default config.",
" external editor command '' specified from environment.",
"\x1b[91m",
f"Error connecting to Zulip server: {server_connection_error}.\x1b[0m",
]
Expand Down Expand Up @@ -481,6 +485,7 @@ def test_successful_main_function_with_config(
f" maximum footlinks value {footlinks_output}",
" color depth setting '256' specified in zuliprc file.",
" notify setting 'enabled' specified in zuliprc file.",
" external editor command '' specified from environment.",
]
assert lines == expected_lines

Expand Down
1 change: 1 addition & 0 deletions tests/core/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def controller(self, mocker: MockerFixture) -> Controller:
color_depth=256,
in_explore_mode=self.in_explore_mode,
debug_path=None,
editor_command="",
**dict(
autohide=self.autohide,
notify=self.notify_enabled,
Expand Down
17 changes: 17 additions & 0 deletions zulipterminal/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
class ConfigSource(Enum):
DEFAULT = "from default config"
ZULIPRC = "in zuliprc file"
ENV = "from environment"
COMMANDLINE = "on command line"


Expand Down Expand Up @@ -80,6 +81,7 @@ class SettingData(NamedTuple):
"color-depth": "256",
"maximum-footlinks": "3",
"exit_confirmation": "enabled",
"editor": "",
}
assert DEFAULT_SETTINGS["autohide"] in VALID_BOOLEAN_SETTINGS["autohide"]
assert DEFAULT_SETTINGS["notify"] in VALID_BOOLEAN_SETTINGS["notify"]
Expand Down Expand Up @@ -553,6 +555,20 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None:
print_setting("maximum footlinks value", zterm["maximum-footlinks"])
print_setting("color depth setting", zterm["color-depth"])
print_setting("notify setting", zterm["notify"])
if zterm["editor"].source == ConfigSource.ZULIPRC:
editor_command = zterm["editor"].value
editor_config_source = ConfigSource.ZULIPRC
else:
editor_command = os.environ.get(
"ZULIP_EDITOR_COMMAND",
os.environ.get("EDITOR", ""),
)
editor_config_source = ConfigSource.ENV

print_setting(
"external editor command",
SettingData(editor_command, editor_config_source),
)

### Generate data not output to user, but into Controller
# Generate urwid palette
Expand All @@ -575,6 +591,7 @@ def print_setting(setting: str, data: SettingData, suffix: str = "") -> None:
in_explore_mode=args.explore,
**boolean_settings,
debug_path=debug_path,
editor_command=editor_command,
).main()
except ServerConnectionFailure as e:
# Acts as separator between logs
Expand Down
5 changes: 5 additions & 0 deletions zulipterminal/config/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,11 @@ class KeyBinding(TypedDict):
'help_text': 'View user information (From Users list)',
'key_category': 'general',
},
'OPEN_EXTERNAL_EDITOR': {
'keys': ['ctrl o'],
'help_text': 'Open the message in external editor',
'key_category': 'msg_compose',
},
'BEGINNING_OF_LINE': {
'keys': ['ctrl a'],
'help_text': 'Jump to the beginning of line',
Expand Down
2 changes: 2 additions & 0 deletions zulipterminal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def __init__(
theme: ThemeSpec,
color_depth: int,
debug_path: Optional[str],
editor_command: str,
in_explore_mode: bool,
autohide: bool,
notify: bool,
Expand All @@ -80,6 +81,7 @@ def __init__(
self.exit_confirmation = exit_confirmation
self.notify_enabled = notify
self.maximum_footlinks = maximum_footlinks
self.editor_command = editor_command

self.debug_path = debug_path

Expand Down
31 changes: 31 additions & 0 deletions zulipterminal/ui_tools/boxes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
"""

import re
import shlex
import shutil
import subprocess
import unicodedata
from collections import Counter
from datetime import datetime, timedelta
from tempfile import NamedTemporaryFile
from time import sleep
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple

Expand Down Expand Up @@ -808,6 +812,33 @@ def keypress(self, size: urwid_Size, key: str) -> Optional[str]:
elif is_command_key("MARKDOWN_HELP", key):
self.view.controller.show_markdown_help()
return key
elif is_command_key("OPEN_EXTERNAL_EDITOR", key):
editor = self.view.controller.editor_command
if editor == "":
self.view.controller.report_error(
"Configure zuliprc file editor key, $EDITOR or "
"$ZULIP_EDITOR_COMMAND shell environment."
)
return key
editor_splits = shlex.split(editor)
fullpath_program = shutil.which(editor_splits[0])
if fullpath_program is None:
self.view.controller.report_error(
"Editor program not found, check $EDITOR "
"or $ZULIP_EDITOR_COMMAND."
)
return key
editor_splits[0] = fullpath_program
with NamedTemporaryFile(suffix=".md") as edit_tempfile:
with open(edit_tempfile.name, mode="w") as edit_writer:
edit_writer.write(self.msg_write_box.edit_text)
self.view.controller.loop.screen.stop()
editor_splits.append(edit_tempfile.name)
subprocess.call(editor_splits)
with open(edit_tempfile.name, mode="r") as edit_reader:
self.msg_write_box.edit_text = edit_reader.read().rstrip()
self.view.controller.loop.screen.start()
return key
elif is_command_key("SAVE_AS_DRAFT", key):
if self.msg_edit_state is None:
if self.compose_box_status == "open_with_private":
Expand Down

0 comments on commit cc1b0e5

Please sign in to comment.