Customize option whitespace-display-mappings. That's what it's for.
M-x customize-option whitespace-display-mappings RET
The doc gives you details (below), but just look at the Customize buffer and things will be even clearer. You can use C-u C-x = with the cursor before a given character, to see its Unicode code point.
C-h v whitespace-display-mappings ...
The docs of standard-display-table refer to buffer-display-table which mentions that you can create display tables with make-display-table if necessary.
This means the following should work:
(let ((display-table (or standard-display-table (make-display-table))))
(set-display-table-slot display-table 'vertical-border (make-glyph-code ?┃))
I found a solution by "abusing" the display table. In the below solution, msg is a string whose length is window width +1 (such as to display the truncation character). I then set the truncation character to the last character of the string.
(set-display-table-slot·buffer-display-table 'wrap ?↩) works for me, with one caveat. In a terminal the arrow shows up correctly, but in the gui (with fringe-mode turned off), the glyph is drawn with a replacement character; apparently it couldn't find a glyph for the arrow in the chosen font. It does render correctly in the scratch buffer, but it's using a ...
Here is a very ugly solution. First, define a function to find hard newlines such that it can be used by font-lock-mode:
(defun whitespace-find-hard-newlines (end)
(search-forward "\n" end t)
(while (and (not (memq 'hard (text-properties-at (match-beginning 0))))
(search-forward "\n" end t)))
(memq 'hard (text-properties-at (match-beginning 0)))...