Skip to content

feat(vim): optional vim-style modal editing in the compose entry#541

Closed
sypianski wants to merge 6 commits into
d99kris:masterfrom
sypianski:master
Closed

feat(vim): optional vim-style modal editing in the compose entry#541
sypianski wants to merge 6 commits into
d99kris:masterfrom
sypianski:master

Conversation

@sypianski

Copy link
Copy Markdown

Closes #535.

What this adds

An optional vim modal editing mode for the message compose entry, enabled by setting vim_mode=1 in ~/.config/nchat/ui.conf. Default is 0 — zero overhead, existing behaviour unchanged.

Commands (normal mode)

Group Commands
Motions h l 0 ^ $, w e b W E B, ( ) sentence, { } paragraph, j k, gg G, f F t T
Operators d c y + any motion; dd cc yy (linewise); D C Y
Text objects iw aw is as ip ap i" a" i' a' i\ a` i( a( i[ a[ i{ a{ i< a<`
Visual v → character-wise selection; d c y x; visual text objects (viw, vi", vi( …)
Edit x X s S o O p P r ~ J
Counts 3w, d3w, 2dd, …
Repeat . repeats last mutating op (including text-object changes: ciw.)
Clipboard p/P sync with system clipboard (X11/Wayland/macOS)

Mode is shown as a colored badge in the status bar and the cursor changes shape (bar in insert, block in normal). Badge colors configurable via color.conf (vim_normal_* / vim_insert_* / vim_visual_*).

Other additions in this branch

  • Send on double-Entersend_on_double_enter=1 in ui.conf (default 0): single Enter inserts newline, second consecutive Enter sends
  • Per-protocol sidebar tints — muted color per protocol (Telegram/WhatsApp/Signal), configurable via color.conf, toggle with list_protocol_colors

Test plan

  • vim_mode=0 (default): no behavioral change, no overhead
  • vim_mode=1: ESC enters normal mode, i/a/A/I returns to insert
  • Motions move cursor correctly in normal mode
  • d/c/y + motion operates on correct range; register available via p
  • Text objects: ciw, di", ya( operate correctly
  • Visual mode: v starts selection, d/y/c acts on it; viw expands to word
  • . repeats last change
  • p pastes text copied from outside nchat (clipboard sync)
  • Badge color updates on mode change; correct badge in status bar
  • send_on_double_enter=1: double Enter sends, single Enter inserts newline

🤖 Generated with Claude Code

@desgua

desgua commented Jun 19, 2026

Copy link
Copy Markdown

@sypianski the last commit of your fork (ab87419) has a problem on the user interface:

  • all the chats appears as if they were "unread",
  • I loose the background of the status and the top bars,

And I have some warnings when compiling:
[ 89%] Building CXX object CMakeFiles/nchat.dir/src/uiview.cpp.o
/home/desgua/Temp/nchat/src/uimodel.cpp: In function ‘bool {anonymous}::VimQuoteObjectRange(const std::wstring&, int, wchar_t, bool, int&, int&)’:
/home/desgua/Temp/nchat/src/uimodel.cpp:313:9: warning: unused variable ‘n’ [-Wunused-variable]
313 | int n = (int)s.size();
| ^
/home/desgua/Temp/nchat/src/uimodel.cpp: In member function ‘void UiModel::Impl::EntryKeyHandler(wint_t)’:
/home/desgua/Temp/nchat/src/uimodel.cpp:836:75: warning: comparison of integer expressions of different signedness: ‘__gnu_cxx::__alloc_traits<std::allocator<wchar_t>, wchar_t>::value_type’ {aka ‘wchar_t’} and ‘wint_t’ {aka ‘unsigned int’} [-Wsign-compare]
836 | if (sendOnDoubleEnter && (entryPos > 0) && (entryStr.at(entryPos - 1) == keyLF))
| ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~
/home/desgua/Temp/nchat/src/uimodel.cpp: In member function ‘void UiModel::Impl::VimRepeatLast(int)’:
/home/desgua/Temp/nchat/src/uimodel.cpp:1141:16: warning: switch missing default case [-Wswitch-default]
1141 | switch (lc.textObjKind)
| ^
/home/desgua/Temp/nchat/src/uimodel.cpp:1041:10: warning: switch missing default case [-Wswitch-default]
1041 | switch (lc.kind)
| ^
/home/desgua/Temp/nchat/src/uimodel.cpp: In member function ‘void UiModel::Impl::VimNormalKey(wint_t)’:
/home/desgua/Temp/nchat/src/uimodel.cpp:1302:67: warning: narrowing conversion of ‘(around ? 97 : 105)’ from ‘wchar_t’ to ‘wint_t’ {aka ‘unsigned int’} [-Wnarrowing]
1302 | m_VimLast = VimLastChange{op, 1, 0, 0, 0, false, around ? L'a' : L'i', p_Key};
| ~~~~~~~^~~~~~~~~~~~~
/home/desgua/Temp/nchat/src/uimodel.cpp:1387:14: warning: switch missing default case [-Wswitch-default]
1387 | switch (findKey) {
| ^
[ 90%] Linking CXX executable bin/nchat
[100%] Built target nchat
-- Ccache stats: 6 hits, 18 misses, 24 total.
[n] /.../nchat $ git log --oneline -n 5
ab87419 (HEAD -> vim_mode_3, sypianski/master, sypianski/HEAD) feat(vim): visual text-objects, c-repeat, clipboard sync
5c650dd feat(vim): r, text-objects, ;/, Y/J/
/V, and . repeat
9b32106 feat(vim): conventional mode badge colors (blue/green/yellow)
4f4d3d5 feat(vim): neutral INSERT badge (protocol color), red NORMAL, violet VISUAL
38a52be fix(vim): high-contrast mode badges over protocol-tinted status bar

@desgua

desgua commented Jun 19, 2026

Copy link
Copy Markdown

I didn't bissect the issue yet, but the first commit 94d1775 doesn't have that problem.

@desgua

desgua commented Jun 19, 2026

Copy link
Copy Markdown

Finished the bisect.

1377466 is the first bad commit
commit 1377466 (HEAD)
Author: Jakub sypianski@outlook.com
Date: Mon Jun 8 17:31:39 2026 +0200

feat(ui): protocol-color the chrome + interlocutor messages

Extend per-protocol coloring per user request:
- Chrome (top bar, status bar, help bar, list/message separator) is
  tinted by the ACTIVE chat's protocol color; redraws on chat switch
  (added UiView::SetTopDirty + dirty hooks).
- Message history: 1:1 received (interlocutor) name + text use the
  chat's protocol color; group chats keep per-sender colors; outgoing
  ("me") messages are neutral default fg (defaultSentColor no longer
  gray).
- Chat list: only the selected row shows the protocol color (reverse
  bar); other chat-name text stays neutral.
- Factor UiColorConfig::GetProtocolColorPair(profileId) shared by all
  views.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

src/uicolorconfig.cpp | 9 ++++++++-
src/uicolorconfig.h | 3 +++
src/uihelpview.cpp | 12 +++++++++++-
src/uihistoryview.cpp | 19 +++++++++++++++++--
src/uilistborderview.cpp | 9 ++-------
src/uilistview.cpp | 8 ++++----
src/uimodel.cpp | 1 +
src/uistatusview.cpp | 11 ++++++++++-
src/uitopview.cpp | 11 ++++++++++-
src/uiview.cpp | 5 +++++
src/uiview.h | 1 +
11 files changed, 72 insertions(+), 17 deletions(-)

sypianski added a commit to sypianski/nchat that referenced this pull request Jun 19, 2026
Reported by desgua (d99kris#541): on the vim_mode_3 branch the status and top
bars lost their background and chat-list rows visually inverted so all
chats looked unread.

Root cause: GetProtocolColorPair returned list_color_<proto> whose _bg
default is empty, which GetColorId maps to -1 (terminal default).
wbkgd then painted the bar with the terminal background instead of the
configured bar background. Same effect tinted list rows past the
configured list_color_bg, so untintable unread rows looked less
prominent than tinted read rows.

Add GetProtocolChromePair(baseParam, profileId) that returns a cached
pair combining the protocol fg with baseParam_bg, so the bar/list/row
keeps its intended background while picking up the protocol hue.
Route status, top, help, listborder, list, and history views through
the new helper.

Also fix the warnings desgua flagged in src/uimodel.cpp:
- drop unused int n in VimQuoteObjectRange
- use wchar_t keyLF = L'\n' to match entryStr value type
- add default: break; to three vim switches
- cast wchar_t literal to wint_t in VimLastChange to silence narrowing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@sypianski

Copy link
Copy Markdown
Author

@desgua thanks for the report — both reproduced from code inspection.

Root cause (chrome bg loss + visual "unread" inversion):
The chrome views (uistatusview / uitopview / uihelpview / uilistborderview) and the chat-list row tinting all called GetProtocolColorPair(profileId), which returns the list_color_<proto> pair. Its _bg default is empty → GetColorId returns -1 (terminal default), so wbkgd(... | colorPair | ' ') overpainted the bar's configured background with the terminal's. Same effect tinted chat-list rows past list_color_bg, so untintable unread rows ended up looking less prominent than tinted read rows.

Fix (dbea82d4 on sypianski/master): new UiColorConfig::GetProtocolChromePair(baseParam, profileId) returns a cached pair combining the protocol fg with baseParam_bg, so each surface keeps its intended bg while picking up the protocol hue. All six call sites routed through it (status / top / help / listborder / list / history).

Compile warnings — fixed in the same commit:

  • uimodel.cpp:313 unused int n → removed
  • uimodel.cpp:836 signed/unsigned compare → wchar_t keyLF = L'\n'
  • uimodel.cpp:1041 / 1141 / 1387 missing default: → added
  • uimodel.cpp:1302 wchar_twint_t narrowing → explicit cast

Please pull and let me know if the bars and chat-list look right now.

@desgua

desgua commented Jun 19, 2026

Copy link
Copy Markdown

*edit: actually it wasn't a bug, it was the configuration of the colors.

@sypianski it seems almost perfect now. But there is still one bug left: all the chats still appear with the foreground color of the unread, as if all of them were unreaded.

I bisect that last bug:

[i] ~/.../nchat $ git bisect good
8b4fc75 is the first bad commit
commit 8b4fc75
Author: Jakub sypianski@outlook.com
Date: Mon Jun 8 16:10:00 2026 +0200

feat(list): subtle per-protocol chat-list tint

Tint chat-list entries by protocol (Telegram / WhatsApp / Signal) using
muted, low-saturation colors close to the terminal's neutral text, so the
network is recognizable at a glance without being loud. Keyed off the
profileId prefix (protocol GetName()).

- ui.conf: list_protocol_colors (default 1) to toggle.
- color.conf: list_color_{telegram,whatsapp,signal}_{fg,bg}; muted hex
  defaults fall back to neutral text on terminals without custom colors.
- Unread chats keep their unread color; tint applies to read chats.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

README.md | 26 ++++++++++++++++++++++++++
src/uicolorconfig.cpp | 9 +++++++++
src/uiconfig.cpp | 1 +
src/uilistview.cpp | 38 ++++++++++++++++++++++++++++++++++++++
4 files changed, 74 insertions(+)

@desgua

desgua commented Jun 20, 2026

Copy link
Copy Markdown

@sypianski I just figured out that the commit I thoght had a bug actually adds a configuration to change the WhatsApp foreground to green and my own configuration was using green for the unread messages. It was just a matter of changing the config file (~/.config/nchat/color.conf) to differentiate read and unread.
The last commit (dbea82d) is working fine. It wasn't a bug. It was the coincidence of adding a new default configuration + my own configuration setting both unread and read messages to the same green color.
Thank you so much for the amazing work at implementing vim-style modal ❤️

@d99kris

d99kris commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Thanks for contributing @sypianski - I hope to find time soon to review this PR.

sypianski and others added 6 commits June 21, 2026 09:06
Add an optional vim mode to the message compose entry, toggled with
`vim_mode=1` in ui.conf. Implements a composable operator+motion engine
rather than hardcoding key combinations.

Engine (uimodel.cpp):
- Motions: h l 0 ^ $ w e b W E B, ( ) sentences, { } paragraphs,
  j k gg G, f F t T find-char. Counts (3w, d3w, 2dd).
- Operators d c y composed with any motion; dd cc yy linewise; D C.
- Edit: x X, s S substitute, o O open-line, p P paste register.
- Visual mode (v) with reverse-video selection highlight, operators
  act on [anchor, cursor].
- Insert/Normal/Visual modes; starts in insert; ESC to normal.

UI feedback:
- Status-bar badge NORMAL/INSERT/VISUAL with configurable colors
  (vim_*_color_bg/fg, vim_*_attr in color.conf).
- DECSCUSR cursor shape: bar in insert, block in normal.
- Visual selection highlighted via second WordWrap pass for anchor
  screen coords (reflow-correct).
- set_escdelay(25) to minimize bare-ESC lag on mode switch.

Config defaults added: vim_mode (ui.conf), vim_*_color/attr (color.conf).
Zero overhead when vim_mode=0 (VimKeyHandler early-returns to the
existing EntryKeyHandler).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add ui.conf option send_on_double_enter (default 0): when enabled, Enter
inserts a newline and a second consecutive Enter sends the message (dropping
the just-typed trailing newline). send_msg key binding is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The status bar is now tinted by protocol (blue/green/magenta), so the vim
mode badges (previously blue/green/magenta) blended in — INSERT on a
Telegram (blue) chat was invisible. Switch badge defaults to white/yellow/red
which contrast with all protocol hues.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switch vim mode badges to the standard vim/airline scheme — NORMAL blue,
INSERT green, VISUAL yellow — using bright variants so they stay distinct
over the protocol-tinted status bar. Drop the protocol-colored INSERT badge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds replace (r<char>), text-objects iw/aw/iW/aW/is/as/ip/ap and
quote/bracket variants (i"/a", i'/a', i\`/a\`, i(/a(/ib, i[/a[, i{/a{/iB,
i</a<) for c/d/y operators, ;/, to repeat last f/F/t/T, Y/J/~ commands,
linewise visual V, and . to repeat the last normal-mode mutation
(excludes change-class commands, which would require insert-mode replay).
- viw/vi"/vi( etc: i/a now works in visual mode to expand selection
  to text object (previously only worked after d/c/y operator)
- ciw/ci"/ci( . repeat: c text-objects now record m_VimLast (was
  only d/y); VimRepeatLast gains case L'c' alongside d/y
- C command now repeatable (sets m_VimLast, case L'C' in repeat)
- p/P sync with system clipboard: yank writes to Clipboard::SetText,
  paste reads Clipboard::GetText() and updates m_VimRegister so text
  copied outside nchat is immediately available via p

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@desgua

desgua commented Jun 21, 2026

Copy link
Copy Markdown

I've been using the implementation for 2 days and it is working flawless and it is incredible useful for those used to vim.

@sypianski

Copy link
Copy Markdown
Author

@desgua — you're right, and I had already separated this into a vim-only branch a while ago but forgot to update the PR. Sorry about that.

New PR with vim-only changes: #543

Closing this one.

@sypianski sypianski closed this Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: optional vim-style modal editing in the compose entry

3 participants