--- /dev/null
+From cc3a7e5566f7a33deeed5cbdcb9057e585c91dde Mon Sep 17 00:00:00 2001
+From: Andrey Proskurin <>
+Date: Sun, 9 May 2021 00:34:16 +0000
+Subject: [PATCH 1/5] view: refactor view_addch
+
+---
+ view.c | 158 ++++++++++++++++++++++++++++-----------------------------
+ 1 file changed, 79 insertions(+), 79 deletions(-)
+
+diff --git a/view.c b/view.c
+index 74967dc6..b10deb92 100644
+--- a/view.c
++++ b/view.c
+@@ -164,98 +164,98 @@ Filerange view_viewport_get(View *view) {
+ return (Filerange){ .start = view->start, .end = view->end };
+ }
+
++static bool view_add_cell(View *view, const Cell *cell) {
++ size_t lineno = view->line->lineno;
++
++ if (view->col + cell->width > view->width) {
++ for (int i = view->col; i < view->width; i++)
++ view->line->cells[i] = view->cell_blank;
++ view->line = view->line->next;
++ view->col = 0;
++ }
++
++ if (!view->line)
++ return false;
++
++ view->line->width += cell->width;
++ view->line->len += cell->len;
++ view->line->lineno = lineno;
++ view->line->cells[view->col] = *cell;
++ view->col++;
++ /* set cells of a character which uses multiple columns */
++ for (int i = 1; i < cell->width; i++)
++ view->line->cells[view->col++] = cell_unused;
++ return true;
++}
++
++static bool view_expand_tab(View *view, Cell *cell) {
++ cell->width = 1;
++
++ int displayed_width = view->tabwidth - (view->col % view->tabwidth);
++ for (int w = 0; w < displayed_width; ++w) {
++
++ int t = (w == 0) ? SYNTAX_SYMBOL_TAB : SYNTAX_SYMBOL_TAB_FILL;
++ const char *symbol = view->symbols[t]->symbol;
++ strncpy(cell->data, symbol, sizeof(cell->data) - 1);
++ cell->len = (w == 0) ? 1 : 0;
++
++ if (!view_add_cell(view, cell))
++ return false;
++ }
++
++ cell->len = 1;
++ return true;
++}
++
++static bool view_expand_newline(View *view, Cell *cell) {
++ const char *symbol = view->symbols[SYNTAX_SYMBOL_EOL]->symbol;
++ strncpy(cell->data, symbol, sizeof(cell->data) - 1);
++ cell->width = 1;
++
++ if (!view_add_cell(view, cell))
++ return false;
++
++ for (int i = view->col; i < view->width; ++i)
++ view->line->cells[i] = view->cell_blank;
++
++ size_t lineno = view->line->lineno;
++ view->line = view->line->next;
++ view->col = 0;
++ if (view->line)
++ view->line->lineno = lineno + 1;
++
++ return true;
++}
++
+ /* try to add another character to the view, return whether there was space left */
+ static bool view_addch(View *view, Cell *cell) {
+ if (!view->line)
+ return false;
+
+- int width;
+- size_t lineno = view->line->lineno;
+ unsigned char ch = (unsigned char)cell->data[0];
+ cell->style = view->cell_blank.style;
+
+ switch (ch) {
+ case '\t':
+- cell->width = 1;
+- width = view->tabwidth - (view->col % view->tabwidth);
+- for (int w = 0; w < width; w++) {
+- if (view->col + 1 > view->width) {
+- view->line = view->line->next;
+- view->col = 0;
+- if (!view->line)
+- return false;
+- view->line->lineno = lineno;
+- }
+-
+- cell->len = w == 0 ? 1 : 0;
+- int t = w == 0 ? SYNTAX_SYMBOL_TAB : SYNTAX_SYMBOL_TAB_FILL;
+- strncpy(cell->data, view->symbols[t]->symbol, sizeof(cell->data)-1);
+- view->line->cells[view->col] = *cell;
+- view->line->len += cell->len;
+- view->line->width += cell->width;
+- view->col++;
+- }
+- cell->len = 1;
+- return true;
++ return view_expand_tab(view, cell);
+ case '\n':
+- cell->width = 1;
+- if (view->col + cell->width > view->width) {
+- view->line = view->line->next;
+- view->col = 0;
+- if (!view->line)
+- return false;
+- view->line->lineno = lineno;
+- }
+-
+- strncpy(cell->data, view->symbols[SYNTAX_SYMBOL_EOL]->symbol, sizeof(cell->data)-1);
+-
+- view->line->cells[view->col] = *cell;
+- view->line->len += cell->len;
+- view->line->width += cell->width;
+- for (int i = view->col + 1; i < view->width; i++)
+- view->line->cells[i] = view->cell_blank;
+-
+- view->line = view->line->next;
+- if (view->line)
+- view->line->lineno = lineno + 1;
+- view->col = 0;
+- return true;
+- default:
+- if (ch < 128 && !isprint(ch)) {
+- /* non-printable ascii char, represent it as ^(char + 64) */
+- *cell = (Cell) {
+- .data = { '^', ch == 127 ? '?' : ch + 64, '\0' },
+- .len = 1,
+- .width = 2,
+- .style = cell->style,
+- };
+- }
+-
+- if (ch == ' ') {
+- strncpy(cell->data, view->symbols[SYNTAX_SYMBOL_SPACE]->symbol, sizeof(cell->data)-1);
+-
+- }
+-
+- if (view->col + cell->width > view->width) {
+- for (int i = view->col; i < view->width; i++)
+- view->line->cells[i] = view->cell_blank;
+- view->line = view->line->next;
+- view->col = 0;
+- }
++ return view_expand_newline(view, cell);
++ case ' ':
++ const char *symbol = view->symbols[SYNTAX_SYMBOL_SPACE]->symbol;
++ strncpy(cell->data, symbol, sizeof(cell->data) - 1);
++ return view_add_cell(view, cell);
++ }
+
+- if (view->line) {
+- view->line->width += cell->width;
+- view->line->len += cell->len;
+- view->line->lineno = lineno;
+- view->line->cells[view->col] = *cell;
+- view->col++;
+- /* set cells of a character which uses multiple columns */
+- for (int i = 1; i < cell->width; i++)
+- view->line->cells[view->col++] = cell_unused;
+- return true;
+- }
+- return false;
++ if (ch < 128 && !isprint(ch)) {
++ /* non-printable ascii char, represent it as ^(char + 64) */
++ *cell = (Cell) {
++ .data = { '^', ch == 127 ? '?' : ch + 64, '\0' },
++ .len = 1,
++ .width = 2,
++ .style = cell->style,
++ };
+ }
++ return view_add_cell(view, cell);
+ }
+
+ static void cursor_to(Selection *s, size_t pos) {
+
+From 50e75ddf8a73feab300d7789d000f9687a509f18 Mon Sep 17 00:00:00 2001
+From: Andrey Proskurin <>
+Date: Sun, 9 May 2021 18:17:20 +0000
+Subject: [PATCH 2/5] view.c: add word wrapping
+
+---
+ view.c | 61 +++++++++++++++++++++++++++++++++++++++++++---------------
+ 1 file changed, 45 insertions(+), 16 deletions(-)
+
+diff --git a/view.c b/view.c
+index b10deb92..e7ca8141 100644
+--- a/view.c
++++ b/view.c
+@@ -80,6 +80,10 @@ struct View {
+ bool need_update; /* whether view has been redrawn */
+ bool large_file; /* optimize for displaying large files */
+ int colorcolumn;
++ // TODO lua option: breakat / brk
++ const char *breakat; /* characters which might cause a word wrap */
++ int wrapcol; /* used while drawing view content, column where word wrap might happen */
++ bool prevch_breakat; /* used while drawing view content, previous char is part of breakat */
+ };
+
+ static const SyntaxSymbol symbols_none[] = {
+@@ -109,6 +113,7 @@ static bool view_viewport_up(View *view, int n);
+ static bool view_viewport_down(View *view, int n);
+
+ static void view_clear(View *view);
++static bool view_add_cell(View *view, const Cell *cell);
+ static bool view_addch(View *view, Cell *cell);
+ static void selection_free(Selection*);
+ /* set/move current cursor position to a given (line, column) pair */
+@@ -156,6 +161,8 @@ static void view_clear(View *view) {
+ view->bottomline->next = NULL;
+ view->line = view->topline;
+ view->col = 0;
++ view->wrapcol = 0;
++ view->prevch_breakat = false;
+ if (view->ui)
+ view->cell_blank.style = view->ui->style_get(view->ui, UI_STYLE_DEFAULT);
+ }
+@@ -164,19 +171,37 @@ Filerange view_viewport_get(View *view) {
+ return (Filerange){ .start = view->start, .end = view->end };
+ }
+
++static void view_wrap_line(View *view) {
++ Line *cur_line = view->line;
++ int cur_col = view->col;
++ int wrapcol = (view->wrapcol > 0) ? view->wrapcol : cur_col;
++
++ view->line = cur_line->next;
++ view->col = 0;
++ view->wrapcol = 0;
++ if (view->line) {
++ /* move extra cells to the next line */
++ for (int i = wrapcol; i < cur_col; ++i) {
++ const Cell *cell = &cur_line->cells[i];
++ view_add_cell(view, cell);
++ cur_line->width -= cell->width;
++ cur_line->len -= cell->len;
++ }
++ }
++ for (int i = wrapcol; i < view->width; ++i) {
++ /* clear remaining of line */
++ cur_line->cells[i] = view->cell_blank;
++ }
++}
++
+ static bool view_add_cell(View *view, const Cell *cell) {
+ size_t lineno = view->line->lineno;
+
+- if (view->col + cell->width > view->width) {
+- for (int i = view->col; i < view->width; i++)
+- view->line->cells[i] = view->cell_blank;
+- view->line = view->line->next;
+- view->col = 0;
+- }
++ if (view->col + cell->width > view->width)
++ view_wrap_line(view);
+
+ if (!view->line)
+ return false;
+-
+ view->line->width += cell->width;
+ view->line->len += cell->len;
+ view->line->lineno = lineno;
+@@ -208,22 +233,18 @@ static bool view_expand_tab(View *view, Cell *cell) {
+ }
+
+ static bool view_expand_newline(View *view, Cell *cell) {
++ size_t lineno = view->line->lineno;
+ const char *symbol = view->symbols[SYNTAX_SYMBOL_EOL]->symbol;
++
+ strncpy(cell->data, symbol, sizeof(cell->data) - 1);
+ cell->width = 1;
+-
+ if (!view_add_cell(view, cell))
+ return false;
+
+- for (int i = view->col; i < view->width; ++i)
+- view->line->cells[i] = view->cell_blank;
+-
+- size_t lineno = view->line->lineno;
+- view->line = view->line->next;
+- view->col = 0;
++ view->wrapcol = 0;
++ view_wrap_line(view);
+ if (view->line)
+ view->line->lineno = lineno + 1;
+-
+ return true;
+ }
+
+@@ -233,8 +254,14 @@ static bool view_addch(View *view, Cell *cell) {
+ return false;
+
+ unsigned char ch = (unsigned char)cell->data[0];
++ bool ch_breakat = strchr(view->breakat, ch);
++ if (view->prevch_breakat && !ch_breakat) {
++ /* this is a good place to wrap line if needed */
++ view->wrapcol = view->col;
++ }
++ view->prevch_breakat = ch_breakat;
+ cell->style = view->cell_blank.style;
+-
++
+ switch (ch) {
+ case '\t':
+ return view_expand_tab(view, cell);
+@@ -519,6 +546,8 @@ View *view_new(Text *text) {
+ .data = " ",
+ };
+ view->tabwidth = 8;
++ // TODO default value
++ view->breakat = "";
+ view_options_set(view, 0);
+
+ if (!view_resize(view, 1, 1)) {
+
+From b50672e3233e5e2d2a537d697082806a5012d6ac Mon Sep 17 00:00:00 2001
+From: Andrey Proskurin <>
+Date: Sun, 9 May 2021 21:56:36 +0000
+Subject: [PATCH 3/5] add `wrapcolumn / wc` and `breakat / brk` options
+
+---
+ man/vis.1 | 5 +++++
+ sam.c | 12 ++++++++++++
+ view.c | 29 +++++++++++++++++++++++------
+ view.h | 2 ++
+ vis-cmds.c | 6 ++++++
+ 5 files changed, 48 insertions(+), 6 deletions(-)
+
+diff --git a/man/vis.1 b/man/vis.1
+index 05433663..2f6b4754 100644
+--- a/man/vis.1
++++ b/man/vis.1
+@@ -1423,6 +1423,11 @@ WARNING: modifying a memory mapped file in-place will cause data loss.
+ Whether to use vertical or horizontal layout.
+ .It Cm ignorecase , Cm ic Op Cm off
+ Whether to ignore case when searching.
++.It Ic wrapcolumn , Ic wc Op Ar 0
++Wrap lines at minimum of window width and wrapcolumn.
++.
++.It Ic breakat , brk Op Dq Pa ""
++Characters which might cause a word wrap.
+ .El
+ .
+ .Sh COMMAND and SEARCH PROMPT
+diff --git a/sam.c b/sam.c
+index 29e9c583..d7540e07 100644
+--- a/sam.c
++++ b/sam.c
+@@ -301,6 +301,8 @@ enum {
+ OPTION_CHANGE_256COLORS,
+ OPTION_LAYOUT,
+ OPTION_IGNORECASE,
++ OPTION_BREAKAT,
++ OPTION_WRAP_COLUMN,
+ };
+
+ static const OptionDef options[] = {
+@@ -394,6 +396,16 @@ static const OptionDef options[] = {
+ VIS_OPTION_TYPE_BOOL,
+ VIS_HELP("Ignore case when searching")
+ },
++ [OPTION_BREAKAT] = {
++ { "breakat", "brk" },
++ VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW,
++ VIS_HELP("Characters which might cause a word wrap")
++ },
++ [OPTION_WRAP_COLUMN] = {
++ { "wrapcolumn", "wc" },
++ VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW,
++ VIS_HELP("Wrap lines at minimum of window width and wrapcolumn")
++ },
+ };
+
+ bool sam_init(Vis *vis) {
+diff --git a/view.c b/view.c
+index e7ca8141..79fc7bc1 100644
+--- a/view.c
++++ b/view.c
+@@ -80,9 +80,9 @@ struct View {
+ bool need_update; /* whether view has been redrawn */
+ bool large_file; /* optimize for displaying large files */
+ int colorcolumn;
+- // TODO lua option: breakat / brk
+- const char *breakat; /* characters which might cause a word wrap */
+- int wrapcol; /* used while drawing view content, column where word wrap might happen */
++ char *breakat; /* characters which might cause a word wrap */
++ int wrapcolumn; /* wrap lines at minimum of window width and wrapcolumn (if != 0) */
++ int wrapcol; /* used while drawing view content, column where word wrap might happen */
+ bool prevch_breakat; /* used while drawing view content, previous char is part of breakat */
+ };
+
+@@ -171,6 +171,12 @@ Filerange view_viewport_get(View *view) {
+ return (Filerange){ .start = view->start, .end = view->end };
+ }
+
++static int view_max_text_width(const View *view) {
++ if (view->wrapcolumn > 0)
++ return MIN(view->wrapcolumn, view->width);
++ return view->width;
++}
++
+ static void view_wrap_line(View *view) {
+ Line *cur_line = view->line;
+ int cur_col = view->col;
+@@ -197,7 +203,7 @@ static void view_wrap_line(View *view) {
+ static bool view_add_cell(View *view, const Cell *cell) {
+ size_t lineno = view->line->lineno;
+
+- if (view->col + cell->width > view->width)
++ if (view->col + cell->width > view_max_text_width(view))
+ view_wrap_line(view);
+
+ if (!view->line)
+@@ -519,6 +525,7 @@ void view_free(View *view) {
+ selection_free(view->selections);
+ free(view->textbuf);
+ free(view->lines);
++ free(view->breakat);
+ free(view);
+ }
+
+@@ -546,8 +553,8 @@ View *view_new(Text *text) {
+ .data = " ",
+ };
+ view->tabwidth = 8;
+- // TODO default value
+- view->breakat = "";
++ view->breakat = strdup("");
++ view->wrapcolumn = 0;
+ view_options_set(view, 0);
+
+ if (!view_resize(view, 1, 1)) {
+@@ -891,6 +898,16 @@ int view_colorcolumn_get(View *view) {
+ return view->colorcolumn;
+ }
+
++void view_wrapcolumn_set(View *view, int col) {
++ if (col >= 0)
++ view->wrapcolumn = col;
++}
++
++void view_breakat_set(View *view, const char *breakat) {
++ free(view->breakat);
++ view->breakat = strdup(breakat);
++}
++
+ size_t view_screenline_goto(View *view, int n) {
+ size_t pos = view->start;
+ for (Line *line = view->topline; --n > 0 && line != view->lastline; line = line->next)
+diff --git a/view.h b/view.h
+index 31b044b8..65bcb29d 100644
+--- a/view.h
++++ b/view.h
+@@ -358,6 +358,8 @@ void view_options_set(View*, enum UiOption options);
+ enum UiOption view_options_get(View*);
+ void view_colorcolumn_set(View*, int col);
+ int view_colorcolumn_get(View*);
++void view_wrapcolumn_set(View*, int col);
++void view_breakat_set(View*, const char *breakat);
+
+ /** Set how many spaces are used to display a tab `\t` character. */
+ void view_tabwidth_set(View*, int tabwidth);
+diff --git a/vis-cmds.c b/vis-cmds.c
+index f5221d14..e2bff70d 100644
+--- a/vis-cmds.c
++++ b/vis-cmds.c
+@@ -364,6 +364,12 @@ static bool cmd_set(Vis *vis, Win *win, Command *cmd, const char *argv[], Select
+ case OPTION_IGNORECASE:
+ vis->ignorecase = toggle ? !vis->ignorecase : arg.b;
+ break;
++ case OPTION_BREAKAT:
++ view_breakat_set(win->view, arg.s);
++ break;
++ case OPTION_WRAP_COLUMN:
++ view_wrapcolumn_set(win->view, arg.i);
++ break;
+ default:
+ if (!opt->func)
+ return false;
+
+From ee36292c44370678f261ea843c3ebcf02fa19156 Mon Sep 17 00:00:00 2001
+From: Andrey Proskurin <>
+Date: Fri, 14 May 2021 16:44:44 +0000
+Subject: [PATCH 4/5] view.c: check return value of strdup
+
+---
+ view.c | 32 +++++++++++++++++---------------
+ view.h | 2 +-
+ vis-cmds.c | 5 ++++-
+ 3 files changed, 22 insertions(+), 17 deletions(-)
+
+diff --git a/view.c b/view.c
+index 79fc7bc1..f1864e8b 100644
+--- a/view.c
++++ b/view.c
+@@ -273,11 +273,11 @@ static bool view_addch(View *view, Cell *cell) {
+ return view_expand_tab(view, cell);
+ case '\n':
+ return view_expand_newline(view, cell);
+- case ' ':
++ case ' ': {
+ const char *symbol = view->symbols[SYNTAX_SYMBOL_SPACE]->symbol;
+ strncpy(cell->data, symbol, sizeof(cell->data) - 1);
+ return view_add_cell(view, cell);
+- }
++ }}
+
+ if (ch < 128 && !isprint(ch)) {
+ /* non-printable ascii char, represent it as ^(char + 64) */
+@@ -541,29 +541,27 @@ View *view_new(Text *text) {
+ View *view = calloc(1, sizeof(View));
+ if (!view)
+ return NULL;
+- view->text = text;
+- if (!view_selections_new(view, 0)) {
+- view_free(view);
+- return NULL;
+- }
+
++ view->text = text;
++ view->tabwidth = 8;
++ view->breakat = strdup("");
++ view->wrapcolumn = 0;
+ view->cell_blank = (Cell) {
+ .width = 0,
+ .len = 0,
+ .data = " ",
+ };
+- view->tabwidth = 8;
+- view->breakat = strdup("");
+- view->wrapcolumn = 0;
+ view_options_set(view, 0);
+
+- if (!view_resize(view, 1, 1)) {
++ if (!view->breakat ||
++ !view_selections_new(view, 0) ||
++ !view_resize(view, 1, 1))
++ {
+ view_free(view);
+ return NULL;
+ }
+-
++
+ view_cursor_to(view, 0);
+-
+ return view;
+ }
+
+@@ -903,9 +901,13 @@ void view_wrapcolumn_set(View *view, int col) {
+ view->wrapcolumn = col;
+ }
+
+-void view_breakat_set(View *view, const char *breakat) {
++bool view_breakat_set(View *view, const char *breakat) {
++ char *copy = strdup(breakat);
++ if (!copy)
++ return false;
+ free(view->breakat);
+- view->breakat = strdup(breakat);
++ view->breakat = copy;
++ return true;
+ }
+
+ size_t view_screenline_goto(View *view, int n) {
+diff --git a/view.h b/view.h
+index 65bcb29d..dadecb48 100644
+--- a/view.h
++++ b/view.h
+@@ -359,7 +359,7 @@ enum UiOption view_options_get(View*);
+ void view_colorcolumn_set(View*, int col);
+ int view_colorcolumn_get(View*);
+ void view_wrapcolumn_set(View*, int col);
+-void view_breakat_set(View*, const char *breakat);
++bool view_breakat_set(View*, const char *breakat);
+
+ /** Set how many spaces are used to display a tab `\t` character. */
+ void view_tabwidth_set(View*, int tabwidth);
+diff --git a/vis-cmds.c b/vis-cmds.c
+index e2bff70d..d3b5f89a 100644
+--- a/vis-cmds.c
++++ b/vis-cmds.c
+@@ -365,7 +365,10 @@ static bool cmd_set(Vis *vis, Win *win, Command *cmd, const char *argv[], Select
+ vis->ignorecase = toggle ? !vis->ignorecase : arg.b;
+ break;
+ case OPTION_BREAKAT:
+- view_breakat_set(win->view, arg.s);
++ if (!view_breakat_set(win->view, arg.s)) {
++ vis_info_show(vis, "Failed to set breakat");
++ return false;
++ }
+ break;
+ case OPTION_WRAP_COLUMN:
+ view_wrapcolumn_set(win->view, arg.i);
+
+From f698e53e4772497c41a12288339e3841dbca9680 Mon Sep 17 00:00:00 2001
+From: Andrey Proskurin <andreyproskurin@protonmail.com>
+Date: Fri, 14 May 2021 18:46:20 +0000
+Subject: [PATCH 5/5] view.c: add utf-8 support to `breakat`
+
+---
+ view.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/view.c b/view.c
+index f1864e8b..a399dd02 100644
+--- a/view.c
++++ b/view.c
+@@ -260,7 +260,7 @@ static bool view_addch(View *view, Cell *cell) {
+ return false;
+
+ unsigned char ch = (unsigned char)cell->data[0];
+- bool ch_breakat = strchr(view->breakat, ch);
++ bool ch_breakat = strstr(view->breakat, cell->data);
+ if (view->prevch_breakat && !ch_breakat) {
+ /* this is a good place to wrap line if needed */
+ view->wrapcol = view->col;