NetBSD is having an influx of new users. Some have mentioned having no
alternative screen editor to vi(1) in base is an obstacle to getting
their system configured. I've been trying to point new users to
xedit(1), but this doesn't help if they're in single-user mode or
can't get X running, or are limited to /rescue.

Before I'm tied to the ducking stool: my preferred editor is vi.
I haven't got any experience with other famous unix editors besides
maybe trying them for 30 seconds 10+ years ago.

Some years ago, I was investigating text editors that take up a small
footprint that would be suitable for a rescue system. I came across
atto, a modeless public domain text editor supporting UTF-8, multiple
buffers, search-and-replace, and region cutting/ copying in less than
2000 lines of non-obfuscated C code. It can use our own libcurses
just fine.

Compiled with GCC 14 and stripped, dynamic executable:

$ size atto
   text    data     bss     dec     hex filename
  29405    4512    2184   36101    8d05 atto

Compiled with GCC 14 and stripped, static executable:

$ size ./atto
   text    data     bss     dec     hex filename
1014373   44024 2171225 3229622  3147b6 atto

Compared to some other editors known for their small footprint,
it's nearly 10 times smaller than nano and joe's own editor.
It is much cleaner than something like pico.

atto is mostly "finished software" since 2017 or so, but received a
couple of commits to its repository every few years (none since 2023):

        https://github.com/hughbarney/atto

I spent a while bringing the code to NetBSD standards. Among other
things, this meant adding standard getopt-based option handling,
making syntax highlighting optional (turned off by default),
fixing some stricter compiler warnings, constification, etc.

I also wrote a manual page. I am a first-time user of this style of
editor, so it was a learning exercise.

I have prepared a patch that integrates atto into NetBSD. It is
attached.

ATTO(1)                  General Commands Manual                 ATTO(1)

NAME
     atto - simple modeless text editor

SYNOPSIS
     atto [-C] file ...

DESCRIPTION
     atto is a screen-based text editor.  It serves as an alternative to
     vi(1) for users who are not accustomed to text editors with
     separate insertion and editing modes, and includes support for
     editing UTF-8 text.

     Perhaps all that needs to be known to use atto at a basic level is
     that holding the control key, pressing x, then pressing w will
     write the currently open file, and holding the control key,
     pressing x, then pressing c will exit the editor.

     The options are as follows:

     -C          Use colours, if available, for basic language-unaware
                 syntax highlighting.

   Cursor movement commands
     C is used as an abbreviation for the control key, commonly rendered
     as "ctrl" on English language keyboards.

     Arrow keys or the navigation cluster can also be used to move the
     cursor when the keyboard has such features.

           C-a        move cursor to beginning of current line (i.e.
                      "home")
           C-b        move cursor back one character (i.e. "left")
           C-e        move cursor to end of current line (i.e. "end")
           C-f        move cursor forward one character (i.e. "right")
           C-n        move cursor to next line (i.e. "down")
           C-p        move cursor to previous line (i.e. "up")
           C-v        move cursor to next page of text (i.e. "page
                      down")
           Esc-b      move cursor back one word
           Esc-f      move cursor forward one word
           Esc-g      jump to a specific line number in the file
           Esc-v      move cursor to previous page of text (i.e. "page
                      up")
           Esc-<      jump to the start of the file
           Esc->      jump to the end of the file

   Text editing commands
           C-d        delete character under cursor (i.e. "del")
           C-h        move cursor backwards and delete previous
                      character (i.e. "backspace")
           C-g        abort at prompts (cancel task when asked a
                      question)
           C-i        insert tab character
           C-j        insert newline character (i.e. "enter", "return")
           C-k        cut text up to the end of the line (i.e. "kill
                      line")
           C-l        refresh the screen
           C-m        insert carriage return character
           C-r        search for text backwards, jump to next result
           C-s        search for text forwards, jump to next result
           C-y        paste the text in the cut buffer (i.e. "yank")
           Esc-r      search for text and replace it

   Buffer commands
     atto supports multiple editing buffers, allowing users to edit
     multiple files from a single instance of the program.

           C-x C-c    close all open buffers and exit atto, prompting if
                      changes would be discarded
           C-x C-f    open a file into a new buffer
           C-x C-k    close the current buffer, prompting if changes
                      would be discarded (i.e. "kill")
           C-x C-s    save current buffer to disk, overwriting the file
           C-x C-w    save current buffer to disk, prompting for file
                      name ("write")
           C-x n      cycle to next buffer
           C-x i      read another file and insert it into the current
                      buffer
           C-x =      display cursor position in current buffer

   Region commands
     As well as cutting text on a line basis (i.e. C-k), a region can be
     selected by setting a mark at the beginning of the region, and then
     moving the cursor to the end of the region.

           C-space    set the beginning of the region at the current
                      position
           C-w        cut the marked region
           Esc-w      copy the marked region without deleting it

   Window management commands
     Simple split-screen window management is included.  For more
     advanced use cases, consider using tmux(1).

           C-x 1      delete windows except the currently active one
           C-x 2      split current window into two windows
           C-x o      switch to the next open window

SEE ALSO
     ed(1), ex(1), vi(1), xedit(1)

STANDARDS
     atto is not the standard editor.

AUTHORS
     atto was written primarily by Hugh Barney, and was originally based
     on public domain code from Anthony Howe's editor (commonly known as
     Anthony's Editor).

     It has been modified for inclusion in NetBSD, including with
     enhanced option handling.

NetBSD 11.99.9              October 15, 2026              NetBSD 11.99.9
? usr.bin/atto/.gdbinit
Index: usr.bin/Makefile
===================================================================
RCS file: /cvsroot/src/usr.bin/Makefile,v
retrieving revision 1.247
diff -u -p -r1.247 Makefile
--- usr.bin/Makefile	28 Dec 2025 16:50:22 -0000	1.247
+++ usr.bin/Makefile	16 Oct 2026 11:30:02 -0000
@@ -3,7 +3,7 @@
 
 .include <bsd.own.mk>
 
-SUBDIR= apply asa at \
+SUBDIR= apply asa at atto \
 	banner base64 basename biff \
 	bzip2 bzip2recover c11 c17 c89 c99 cal calendar cap_mkdb cdplay \
 	checknr chflags chpass cksum cmp cleantags col colcrt colrm \
Index: distrib/sets/lists/base/mi
===================================================================
RCS file: /cvsroot/src/distrib/sets/lists/base/mi,v
retrieving revision 1.1390
diff -u -p -r1.1390 mi
--- distrib/sets/lists/base/mi	25 Aug 2026 13:39:55 -0000	1.1390
+++ distrib/sets/lists/base/mi	16 Oct 2026 11:30:02 -0000
@@ -540,6 +540,7 @@
 ./usr/bin/atf2kyua				base-kyua-bin		kyua
 ./usr/bin/atq					base-cron-bin
 ./usr/bin/atrm					base-cron-bin
+./usr/bin/atto					base-util-bin
 ./usr/bin/audiocfg				base-audio-bin		audio
 ./usr/bin/audioctl				base-audio-bin		audio
 ./usr/bin/audioplay				base-audio-bin		audio
Index: distrib/sets/lists/man/mi
===================================================================
RCS file: /cvsroot/src/distrib/sets/lists/man/mi,v
retrieving revision 1.1844
diff -u -p -r1.1844 mi
--- distrib/sets/lists/man/mi	7 Sep 2026 07:43:42 -0000	1.1844
+++ distrib/sets/lists/man/mi	16 Oct 2026 11:30:02 -0000
@@ -74,6 +74,7 @@
 ./usr/share/man/cat1/atf2kyua.0			man-kyua-catman		.cat,kyua
 ./usr/share/man/cat1/atq.0			man-cron-catman		.cat
 ./usr/share/man/cat1/atrm.0			man-cron-catman		.cat
+./usr/share/man/cat1/atto.0			man-util-catman		.cat
 ./usr/share/man/cat1/audiocfg.0			man-audio-catman	audio,.cat
 ./usr/share/man/cat1/audioctl.0			man-audio-catman	audio,.cat
 ./usr/share/man/cat1/audioplay.0		man-audio-catman	audio,.cat
@@ -3682,6 +3683,7 @@
 ./usr/share/man/man1/atf2kyua.1			man-kyua-man		.man,kyua
 ./usr/share/man/man1/atq.1			man-cron-man		.man
 ./usr/share/man/man1/atrm.1			man-cron-man		.man
+./usr/share/man/man1/atto.1			man-util-man		.man
 ./usr/share/man/man1/audiocfg.1			man-audio-man		audio,.man
 ./usr/share/man/man1/audioctl.1			man-audio-man		audio,.man
 ./usr/share/man/man1/audioplay.1		man-audio-man		audio,.man
Index: distrib/sets/lists/manhtml/mi
===================================================================
RCS file: /cvsroot/src/distrib/sets/lists/manhtml/mi,v
retrieving revision 1.61
diff -u -p -r1.61 mi
--- distrib/sets/lists/manhtml/mi	7 Sep 2026 07:43:42 -0000	1.61
+++ distrib/sets/lists/manhtml/mi	16 Oct 2026 11:30:02 -0000
@@ -34,6 +34,7 @@
 ./usr/share/man/html1/atf2kyua.html		man-kyua-htmlman	html,kyua
 ./usr/share/man/html1/atq.html			man-cron-htmlman	html
 ./usr/share/man/html1/atrm.html			man-cron-htmlman	html
+./usr/share/man/html1/atto.html			man-util-htmlman	html
 ./usr/share/man/html1/audiocfg.html		man-audio-htmlman	audio,html
 ./usr/share/man/html1/audioctl.html		man-audio-htmlman	audio,html
 ./usr/share/man/html1/audioplay.html		man-audio-htmlman	audio,html
Index: usr.bin/atto/CHANGE.LOG.md
===================================================================
RCS file: usr.bin/atto/CHANGE.LOG.md
diff -N usr.bin/atto/CHANGE.LOG.md
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/CHANGE.LOG.md	16 Oct 2026 11:30:02 -0000
@@ -0,0 +1,122 @@
+# Atto Emacs Change Log
+
+## Atto v1.22 06 Dec 2020
+* fixed some color issues when running on Arch, needed to set color in modeline and msg line
+
+## Atto v1.21 27 Aug 2020
+* small fix, reframe on insert if point is sitting on EOF and have scrolled off end of window
+
+## Atto v1.20 30 Mar 2018
+* very small enhancement to previous fix.  When we go to the end of the file we do not reframe if the end of file is already displayed in the window.
+
+## Atto v1.19 9 Mar 2018
+* fixed issue with display reframe when last line is displayed and text has been cut above it. On scrolling to end of buffer there was a nasty jump to frame the screen with the last line anchored to the end of the screen. This issue was present in the original Anthonys Editor.
+
+## Atto v1.18 1 Feb 2018
+* fixed bug with opening file ~/.xxx where filename completion has not been used. If ~ is detected we force a call as if the user had selected filename completion.
+
+## Atto v1.17 20 Jan 2018
+* fixed bug with lnend() when at end of buffer, fixed killtoeol() behaviour
+
+## Atto V1.16 25 Nov 2017
+* merged in file descriptor leak from and code reductions in complete.c from "The Infinnovation team"
+* small changes in complete.c to reduce line count
+
+## Atto v1.15.1 25 Nov 2017
+* small code optimisation to reduce line count
+
+## Atto v1.15 29 July 2017
+* Merged in fixes for multi-byte/wide chars, backspace and cursor position provided by Matt Fielding (Magnetic Realms)
+* Added simplified chinese test file to docs
+
+## Atto v1.14 14 April 2017
+* added alternative home and end key bindings (some systems differ in setup)
+* removed some non emacs bindings 
+* re-order definition of keys
+
+## Atto v1.13 13 April 2017
+* fixed problem with set_parse_state.  To be sure of setting hilite state we parse the text up to point at the top of the screen.
+* fixed problem with complete.c not initialising response buffer
+* added resize-terminal
+
+## Atto v1.12 12 April 2017
+* added UTF8 support
+
+## Atto v1.11 11 April 2017
+* reduced some functions to 1 liners in command.c to make space for future UTF8 fixes
+
+## Atto v1.10 9 March 2017
+* discard unbound keystrokes, compacted 1 line functions in command.c
+
+## Atto v1.9 9 March 2017
+* Fixed cut, copy code to avoid corruption as pointer p should be set after call to movegap()
+
+## Atto v1.8 22 December 2016
+* Added generic syntax highlighting, removed the basic undo command to keep code count under 2000 lines.
+* made searchtext start from empty string (as per GNU emacs)
+* modified search_forward() and search_backwards() so that they can be called on any buffer (not just current buf)
+* fixed failed search when first char in buffer should match.
+* some comments reduced to 1 line to save precious line count
+* added flag to getinput() to clear the response if required
+
+## Atto v1.7,  12 October 2016
+* Added filename completion
+NOTE: If this creates a problem on windows the change can be backed out by uncommenting the line in command.c readfile() and using the getinput() instead of getfilename() functions.
+
+## Atto v1.6,  29 December 2015
+* Fixed display problem when editing same buffer in multiple windows.
+* Fixed overflow of filename variable when a large filename is supplied at the command line
+* Reformatted comments at top of source files to claw back 30 more lines for defect fixes
+
+## Atto v1.5,  20 December 2015
+* Added INS = toggle-overwrite-mode
+* flushed keyboard on detection of Esc in search and replace, which means if you touch the arrow keys you exit cleanly without writing the rest of an escape sequence into the buffer.
+* Added entries for escape key binding (eg esc-v and esc-V) to handle when CAPSLOCK is active.
+* Handled CAPSLOCK when prompted y/n to exit.
+* Line count is 1987.
+
+## Atto v1.4.3, 15 December 2015
+* fixed bug with display of last line
+
+## Atto v1.4.2, 13 December 2015
+* Fixed crash bug with free_windows on MS windows, thanks to Ed Davies for reporting
+* went through all files and ensure correct indentation and tabstops, ensured if (foo) { opening brace style
+* line count is 1955
+
+## Atto v1.4.1, 12 December 2015
+* Added esc-@ as alternative to C-space for set-mark
+
+## Atto v1.4 8 December 2015
+* Working Atto that supports multiple windows. It all fits in 1969 lines of C !
+* Fixed bug that meant that scrolling to end of file in other than the first window jumped off the screen.
+* Removed previous-buffer which was not going to get used.
+* Removed redundant definitions of FIRST_LINE
+* Corrected PgDn and PgUp to work with variable size windows.
+
+## Atto V1.3.1 5 December 2015
+* Fixed bug with count_buffers causing kill-buffer to core dump
+
+## Atto V1.3 1 December 2015
+* Added M-r search and replace
+* Added M-g goto-line
+* Updated show-pos with line/total_lines count
+* fixed bug introduced in 1.2 where last modified buffer would not trigger the prompt to save.
+* code footprint is 1775 lines !
+
+## Atto v1.2 29 November 2015
+* Implemented multibuffer support, added approx 180 lines of code
+* First buffer created is called *scratch*
+* Creates *scratch* buffer if you want to delete last buffer
+* Added C-x C-n next-buffer
+* Added C-x C-p prev-buffer
+* Added C-x k   delete-buffer
+* Consumed key.h into header.h
+* code footprint is 1586 lines !
+
+## Atto v1.1 26 November 2015
+* Reduced code footprint by simplification of key code and the definition of keymap. This means I dont have to edit more than one map to add a function.
+* Added forward and reverse text search.
+* Code footprint is less than 1400 lines !
+
+## Atto v1.0 2015
+
Index: usr.bin/atto/Makefile
===================================================================
RCS file: usr.bin/atto/Makefile
diff -N usr.bin/atto/Makefile
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/Makefile	16 Oct 2026 11:30:02 -0000
@@ -0,0 +1,16 @@
+#	$NetBSD$
+
+.include <bsd.own.mk>
+
+PROG=	atto
+SRCS=	command.c display.c gap.c key.c search.c buffer.c replace.c
+SRCS+=	window.c complete.c hilite.c main.c
+
+LDADD+=	-lcurses -lterminfo
+DPADD+=	${LIBCURSES} ${LIBTERMINFO}
+
+WARNS=	3
+
+COPTS.replace.c+=	${CC_WNO_FORMAT_TRUNCATION}
+
+.include <bsd.prog.mk>
Index: usr.bin/atto/README.md
===================================================================
RCS file: usr.bin/atto/README.md
diff -N usr.bin/atto/README.md
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/README.md	16 Oct 2026 11:30:02 -0000
@@ -0,0 +1,185 @@
+# Atto Emacs
+
+The smallest functional Emacs in less than 2000 lines of C.
+
+Atto Emacs is inspired by MicroEmacs, Nano, Pico and my earlier project known as Perfect Emacs [1].
+
+
+![Atto screenshot](https://github.com/hughbarney/atto/blob/master/screenshots/atto.png)
+
+
+> A designer knows he has achieved perfection not when there is nothing left to add, but when there is nothing left to take away.
+> -- <cite>Antoine de Saint-Exupery</cite>
+
+## Goals of Atto Emacs
+
+* Be the smallest fuctional Emacs in less than 2000 lines of C.
+* Provide a rich level of functionality in the smallest amount of code
+* Be easy to understand without extensive study (to encourage further experimentation).
+
+In Defining Atto as the lowest functional Emacs I have had to consider the essential feature set that makes Emacs, 'Emacs'.  I have defined this point as a basic Emacs command set and key bindings; the ability to edit multiple files (buffers), and switch between them; edit the buffers in multiple windows, cut, copy and paste; forward and reverse searching, a replace function, basic syntax highlighting and UTF8 support. The proviso being that all this will fit in less than 2000 lines of C.
+
+
+## Why the name Atto?
+
+The small Emacs naming scheme appears to use sub-unit prefixes in descending order with each further reduction of functionality.  Atto means 10 to the power of minus 18.   Logically Femto (10^-15) comes after Pico (10^-12). However choosing Atto allows for the potential for **Femto** to be an Atto based Emacs with a scripting language.
+
+* As of November 2017 Atto's big brother **Femto** can be found [here](https://github.com/hughbarney/femto)
+
+## Derivation
+
+Atto is based on the public domain code of Anthony Howe's editor (commonly known as Anthony's Editor or AE, [2]).  Rather than representing a file as a linked list of lines, the AE Editor uses the concept of a Buffer-Gap [4,5,6].  A Buffer-Gap editor stores the file in a single piece of contiguous memory with some extra unused space known as the buffer gap.  On character insertion and deletion the gap is first moved to the current point.  A character deletion then extends the gap by moving the gap pointer back by 1 OR the gap is reduced by 1 when a character is inserted.  The Buffer-Gap technique is elegant and significantly reduces the amount of code required to load a file, modify it and redraw the display.  The proof of this is seen when you consider that Atto supports almost the same command set that Pico supports,  but Pico requires almost 17 times the amount of code.
+
+## Comparisons with Other Emacs Implementations
+
+    Editor         Binary   BinSize     KLOC  Files
+
+    atto           atto       33002     1.9k     10
+    pEmacs         pe         59465     5.7K     16
+    Esatz-Emacs    ee         59050     5.7K     14
+    GNOME          GNOME      55922     9.8k     13
+    Zile           zile      257360    11.7k     48
+    Mg             mg        585313    16.5K     50
+    uEmacs/Pk      em        147546    17.5K     34
+    Pico           pico      438534    24.0k     29
+    Nano           nano      192008    24.8K     17
+    jove           jove      248824    34.7k     94
+    Qemacs         qe        379968    36.9k     59
+    ue3.10         uemacs    171664    52.4K     16
+    GNUEmacs       emacs   14632920   358.0k    186
+
+## Atto Key Bindings
+
+    C-A   begining-of-line
+    C-B   backward-character
+    C-D   delete-char
+    C-E   end-of-line
+    C-F   forward Character
+    C-G	  Abort (at prompts)
+    C-H   backspace
+    C-I   handle-tab
+    C-J   newline
+    C-K   kill-to-eol
+    C-L   refresh display
+    C-M   Carrage Return
+    C-N   next line
+    C-P   previous line
+    C-R   search-backwards
+    C-S	  search-forwards
+    C-U   Undo
+    C-V   Page Down
+    C-W   Kill Region (Cut)
+    C-X   CTRL-X command prefix
+    C-Y   Yank (Paste)
+
+    M-<   Start of file
+    M->   End of file
+    M-v   Page Up
+    M-f   Forward Word
+    M-b   Backwards Word
+    M-g   goto-line
+    M-r   Search and Replace
+    M-w   copy-region
+
+    C-<spacebar> Set mark at current position.
+
+    ^X^C  Exit. Any unsaved files will require confirmation.
+    ^X^F  Find file; read into a new buffer created from filename.
+    ^X^S  Save current buffer to disk, using the buffer's filename as the name of
+    ^X^W  Write current buffer to disk. Type in a new filename at the prompt to
+    ^Xi   Insert file at point
+    ^X=   Show Character at position
+    ^X^N  next-buffer
+    ^Xn   next-buffer
+    ^Xk   kill-buffer
+    ^X1   delete-other-windows
+    ^X2   split-window
+    ^Xo   other-window
+
+    Home  Beginning-of-line
+    End   End-of-line
+    Del   Delete character under cursor
+    Ins   Toggle Overwrite Mode
+    Left  Move left
+    Right Move point right
+    Up    Move to the previous line
+    Down  Move to the next line
+    Backspace delete character on the left
+    Ctrl+Up      beginning of file
+    Ctrl+Down    end of file
+    Ctrk+Left    Page Down
+    Ctrl+Right   Page Up
+
+### Copying and moving
+
+    C-<spacebar> Set mark at current position
+    ^W   Delete region
+    ^Y   Yank back kill buffer at cursor
+    M-w  Copy Region
+
+A region is defined as the area between this mark and the current cursor position. The kill buffer is the text which has been most recently deleted or copied.
+
+Generally, the procedure for copying or moving text is:
+1. Mark out region using M-<spacebar> at the beginning and move the cursor to the end.
+2. Delete it (with ^W) or copy it (with M-W) into the kill buffer.
+3. Move the cursor to the desired location and yank it back (with ^Y).
+
+### Searching
+
+    C-S or C-R enters the search prompt, where you type the search string
+    BACKSPACE - will reduce the search string, any other character will extend it
+    C-S at the search prompt will search forward, will wrap at end of the buffer
+    C-R at the search prompt will search backwards, will wrap at start of the buffer
+    ESC will escape from the search prompt and return to the point of the match
+    C-G abort the search and return to point before the search started
+
+## Building on Ubuntu
+
+When building on Ubuntu you will need to install the libcurses dev package.
+
+    $ sudo apt-get install apt-file
+    $ apt-file update
+
+now search for which package would have curses.h and install it:
+
+    $ apt-file search curses.h
+    libncurses5-dev: /usr/include/curses.h
+    $ sudo apt-get install libncurses5-dev
+
+## Future Enhancements and Collaboration
+
+The priority now is bug fixes and keeping the code count below 2000 lines. Bug fixes and optimisations are welcome especially if they deliver code reductions which make space for further fixes.
+
+## Multiple Windows or Not?
+
+Atto supports multiple windows !  This was the hardest part of the project to get working reliably.
+
+The lack of multiple windows would have been quickly noticed as it is a very visible feature of the Emacs user interface.  It is very useful to be able to look at some code in one window whilst editing another section of the same file (or a different file) in another window.  As more than one window can access the same buffer the current point now has now to be associated with the window structure and updated back to the buffer structure whenever any gap or display code is called that accesses the point location. The strategy I used in the end was to treat the buffer as the master and update the window structure with copies of the critical values (point, page, epage, cursor row & col) after each display update of that window.  This is because the display code does the calculations necessary to reframe the screen when the point scrolls up off the screen or below the screen. Getting everything to work correctly when displaying the same buffer in more that one winow was a real challenge and took arpund 15-20 hours to get it working.
+
+A multi-window display issue (specifically evident in a buffer-gap editor) was resolved in Atto 1.6.  This is where you are editing a file that is displayed in more than one window.  Say you are in window1 and delete 3 lines, the current point in the other windows (if the point is below the point in window1) have to be adjusted to take into account that their relative positions in the buffer have now shifted up. We do this by tracking the size of the text in the buffer before and after each command.  At the start of the display function we can work out the difference and adjust the other windows when they are updated. This mechanism works well even when inserting a text file that means that the gap has to be re-allocated.
+
+
+## Known Issues
+
+* Goto-line will fail to go to the very last line.  This is a special case that could easily be fixed.
+
+## Copying
+
+  Atto code is released to the public domain.
+  hughbarney AT gmail.com 2017
+
+## Acknowledgements
+    Ed Davies for bringing Athony's Editor to my attention
+    Anthony Howe for his original codebase
+    Matt Fielding (Magnetic Realms) for providing fixes for multi-byte / wide characters, delete, backspace and cursor position
+    The Infinnovation team for bug fixes to complete.c
+
+## References
+
+    [1] Perfect Emacs - https://github.com/hughbarney/pEmacs
+    [2] Anthony's Editor - https://github.com/hughbarney/Anthony-s-Editor
+    [3] MG - https://github.com/rzalamena/mg
+    [4] Jonathan Payne, Buffer-Gap: http://ned.rubyforge.org/doc/buffer-gap.txt
+    [5] Anthony Howe,  http://ned.rubyforge.org/doc/editor-101.txt
+    [6] Anthony Howe, http://ned.rubyforge.org/doc/editor-102.txt
+
Index: usr.bin/atto/atto.1
===================================================================
RCS file: usr.bin/atto/atto.1
diff -N usr.bin/atto/atto.1
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/atto.1	16 Oct 2026 11:30:02 -0000
@@ -0,0 +1,165 @@
+.\" $NetBSD$
+.\" This file is released under the same public domain license as the
+.\" rest of the atto editor.
+.Dd October 15, 2026
+.Dt ATTO 1
+.Os
+.Sh NAME
+.Nm atto
+.Nd simple modeless text editor
+.Sh SYNOPSIS
+.Nm atto
+.Op Fl C
+.Ar
+.Sh DESCRIPTION
+.Nm
+is a screen-based text editor.
+It serves as an alternative to
+.Xr vi 1
+for users who are not accustomed to text editors with separate insertion
+and editing modes, and includes support for editing UTF-8 text.
+.Pp
+Perhaps all that needs to be known to use
+.Nm
+at a basic level is that holding the control key, pressing x,
+then pressing w will write the currently open file, and holding the
+control key, pressing x, then pressing c will exit the editor.
+.Pp
+The options are as follows:
+.Pp
+.Bl -tag -compact -width Fl
+.It Fl C
+Use colours, if available, for basic language-unaware syntax highlighting.
+.El
+.Ss Cursor movement commands
+C is used as an abbreviation for the control key, commonly rendered as
+"ctrl" on English language keyboards.
+.Pp
+Arrow keys or the navigation cluster can also be used to move the
+cursor when the keyboard has such features.
+.Pp
+.Bl -tag -width "xxspacexx" -compact -offset indent
+.It C-a
+move cursor to beginning of current line (i.e. "home")
+.It C-b
+move cursor back one character (i.e. "left")
+.It C-e
+move cursor to end of current line (i.e. "end")
+.It C-f
+move cursor forward one character (i.e. "right")
+.It C-n
+move cursor to next line (i.e. "down")
+.It C-p
+move cursor to previous line (i.e. "up")
+.It C-v
+move cursor to next page of text (i.e. "page down")
+.It Esc-b
+move cursor back one word
+.It Esc-f
+move cursor forward one word
+.It Esc-g
+jump to a specific line number in the file
+.It Esc-v
+move cursor to previous page of text (i.e. "page up")
+.It Esc-<
+jump to the start of the file
+.It Esc->
+jump to the end of the file
+.El
+.Ss Text editing commands
+.Bl -tag -width "xxspacexx" -compact -offset indent
+.It C-d
+delete character under cursor (i.e. "del")
+.It C-h
+move cursor backwards and delete previous character (i.e. "backspace")
+.It C-g
+abort at prompts (cancel task when asked a question)
+.It C-i
+insert tab character
+.It C-j
+insert newline character (i.e. "enter", "return")
+.It C-k
+cut text up to the end of the line (i.e. "kill line")
+.It C-l
+refresh the screen
+.It C-m
+insert carriage return character
+.It C-r
+search for text backwards, jump to next result
+.It C-s
+search for text forwards, jump to next result
+.It C-y
+paste the text in the cut buffer (i.e. "yank")
+.It Esc-r
+search for text and replace it
+.El
+.Ss Buffer commands
+.Nm
+supports multiple editing buffers, allowing users to edit multiple
+files from a single instance of the program.
+.Pp
+.Bl -tag -width "xxspacexx" -compact -offset indent
+.It C-x C-c
+close all open buffers and exit
+.Nm ,
+prompting if changes would be discarded
+.It C-x C-f
+open a file into a new buffer
+.It C-x C-k
+close the current buffer, prompting if changes would be
+discarded (i.e. "kill")
+.It C-x C-s
+save current buffer to disk, overwriting the file
+.It C-x C-w
+save current buffer to disk, prompting for file name ("write")
+.It C-x n
+cycle to next buffer
+.It C-x i
+read another file and insert it into the current buffer
+.It C-x =
+display cursor position in current buffer
+.El
+.Ss Region commands
+As well as cutting text on a line basis (i.e. C-k), a region
+can be selected by setting a mark at the beginning of the
+region, and then moving the cursor to the end of the region.
+.Pp
+.Bl -tag -width "xxspacexx" -compact -offset indent
+.It C-space
+set the beginning of the region at the current position
+.It C-w
+cut the marked region
+.It Esc-w
+copy the marked region without deleting it
+.El
+.Ss Window management commands
+Simple split-screen window management is included.
+For more advanced use cases, consider using
+.Xr tmux 1 .
+.Pp
+.Bl -tag -width "xxspacexx" -compact -offset indent
+.It C-x 1
+delete windows except the currently active one
+.It C-x 2
+split current window into two windows
+.It C-x o
+switch to the next open window
+.El
+.Sh SEE ALSO
+.Xr ed 1 ,
+.Xr ex 1 ,
+.Xr vi 1 ,
+.Xr xedit 1
+.Sh STANDARDS
+.Nm
+is not the standard editor.
+.Sh AUTHORS
+.Nm
+was written primarily by
+.An Hugh Barney ,
+and was originally based on public domain code from Anthony Howe's
+editor (commonly known as Anthony's Editor).
+.Pp
+It has been modified for inclusion in
+.Nx ,
+including with enhanced option handling.
Index: usr.bin/atto/buffer.c
===================================================================
RCS file: usr.bin/atto/buffer.c
diff -N usr.bin/atto/buffer.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/buffer.c	16 Oct 2026 11:30:02 -0000
@@ -0,0 +1,127 @@
+/* buffer.c, Atto Emacs, Hugh Barney, Public Domain, 2015 */
+
+#include <assert.h>
+#include <string.h>
+#include "header.h"
+
+void buffer_init(buffer_t *bp)
+{
+	bp->b_mark = NOMARK;
+	bp->b_point = 0;
+	bp->b_cpoint = 0;
+	bp->b_page = 0;
+	bp->b_epage = 0;
+	bp->b_reframe = 0;
+	bp->b_size = 0;
+	bp->b_psize = 0;	
+	bp->b_flags = 0;
+	bp->b_cnt = 0;
+	bp->b_buf = NULL;
+	bp->b_ebuf = NULL;
+	bp->b_gap = NULL;
+	bp->b_egap = NULL;
+	bp->b_next = NULL;
+	bp->b_fname[0] = '\0';
+}
+
+/* Find a buffer by filename or create if requested */
+buffer_t* find_buffer (const char *fname, int cflag)
+{
+	buffer_t *bp = NULL;
+	buffer_t *sb = NULL;
+	
+	bp = bheadp;
+	while (bp != NULL) {
+		if (strcmp (fname, bp->b_fname) == 0 || strcmp(fname, bp->b_bname) == 0) {
+			return (bp);
+		}
+		bp = bp->b_next;
+	}
+
+	if (cflag != FALSE) {
+		if ((bp = (buffer_t *) malloc (sizeof (buffer_t))) == NULL)
+			return (0);
+
+		buffer_init(bp);
+		assert(bp != NULL);
+		
+		/* find the place in the list to insert this buffer */
+		if (bheadp == NULL) {
+			bheadp = bp;
+		} else if (strcmp (bheadp->b_fname, fname) > 0) {
+			/* insert at the beginning */
+			bp->b_next = bheadp;
+			bheadp = bp;
+		} else {
+			for (sb = bheadp; sb->b_next != NULL; sb = sb->b_next)
+				if (strcmp (sb->b_next->b_fname, fname) > 0)
+					break;
+			
+			/* and insert it */
+			bp->b_next = sb->b_next;
+			sb->b_next = bp;
+		}
+	}
+	return bp;
+}
+
+/* unlink from the list of buffers, free associated memory, assumes buffer has been saved if modified */
+int delete_buffer (buffer_t *bp)
+{
+	buffer_t *sb = NULL;
+
+	/* we must have switched to a different buffer first */
+	assert(bp != curbp);
+	
+	/* if buffer is the head buffer */
+	if (bp == bheadp) {
+		bheadp = bp->b_next;
+	} else {
+		/* find place where the bp buffer is next */
+		for (sb = bheadp; sb->b_next != bp && sb->b_next != NULL; sb = sb->b_next)
+			;
+		assert(sb->b_next == bp || sb->b_next == NULL);
+		sb->b_next = bp->b_next;
+	}
+
+	/* now we can delete */
+	free(bp->b_buf);
+	free(bp);
+	return TRUE;
+}
+
+void next_buffer(void)
+{
+	assert(curbp != NULL);
+	assert(bheadp != NULL);
+	disassociate_b(curwp);	
+	curbp = (curbp->b_next != NULL ? curbp->b_next : bheadp);
+	associate_b2w(curbp,curwp);
+}
+
+char* get_buffer_name(buffer_t *bp)
+{
+	return (strlen(bp->b_fname) > 0) ? bp->b_fname : bp->b_bname;
+}
+
+int count_buffers(void)
+{
+	buffer_t* bp;
+	int i;
+
+	for (i=0, bp=bheadp; bp != NULL; bp = bp->b_next)
+		i++;
+
+	return i;
+}
+
+int modified_buffers(void)
+{
+	buffer_t* bp;
+
+	for (bp=bheadp; bp != NULL; bp = bp->b_next)
+		if (bp->b_flags & B_MODIFIED)
+			return TRUE;
+
+	return FALSE;
+}
Index: usr.bin/atto/command.c
===================================================================
RCS file: usr.bin/atto/command.c
diff -N usr.bin/atto/command.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/command.c	16 Oct 2026 11:30:02 -0000
@@ -0,0 +1,363 @@
+/* command.c, Atto Emacs, Public Domain, Hugh Barney, 2016, Derived from: Anthony's Editor January 93 */
+
+#include "header.h"
+
+void quit(void) { done = 1; }
+void up(void) { curbp->b_point = lncolumn(curbp, upup(curbp, curbp->b_point),curbp->b_col); }
+void down(void) { curbp->b_point = lncolumn(curbp, dndn(curbp, curbp->b_point),curbp->b_col); }
+void lnbegin(void) { curbp->b_point = segstart(curbp, lnstart(curbp,curbp->b_point), curbp->b_point); }
+void version(void) { msg(VERSION); }
+void top(void) { curbp->b_point = 0; }
+void bottom(void) { curbp->b_point = pos(curbp, curbp->b_ebuf); if (curbp->b_epage < pos(curbp, curbp->b_ebuf)) curbp->b_reframe = 1;}
+void block(void) { curbp->b_mark = curbp->b_point; }
+void copy(void) { copy_cut(FALSE); }
+void cut(void) { copy_cut(TRUE); }
+void resize_terminal(void) { one_window(curwp); }
+
+void quit_ask(void)
+{
+	if (modified_buffers() > 0) {
+		mvaddstr(MSGLINE, 0, "Modified buffers exist; really exit (y/n) ?");
+		clrtoeol();
+		if (!yesno(FALSE))
+			return;
+	}
+	quit();
+}
+
+/* flag = default answer, FALSE=n, TRUE=y */
+int yesno(int flag)
+{
+	int ch;
+
+	addstr(flag ? " y\b" : " n\b");
+	refresh();
+	ch = getch();
+	if (ch == '\r' || ch == '\n')
+		return (flag);
+	return (tolower(ch) == 'y');
+}
+
+void redraw(void)
+{
+	window_t *wp;
+	
+	clear();
+	for (wp=wheadp; wp != NULL; wp = wp->w_next)
+		wp->w_update = TRUE;
+	update_display();
+}
+
+void left(void)
+{
+	int n = prev_utf8_char_size();
+	while (0 < curbp->b_point && n-- > 0)
+		--curbp->b_point;
+}
+
+void right(void)
+{
+	int n = utf8_size(*ptr(curbp,curbp->b_point));
+	while ((curbp->b_point < pos(curbp, curbp->b_ebuf)) && n-- > 0)
+		++curbp->b_point;
+}
+
+/* work out number of bytes based on first byte */
+int utf8_size(char_t c)
+{
+	if (c >= 192 && c < 224) return 2;
+	if (c >= 224 && c < 240) return 3;
+	if (c >= 240 && c < 248) return 4;
+	return 1; /* if in doubt it is 1 */
+}
+
+int prev_utf8_char_size(void)
+{
+	int n;
+	for (n=2;n<5;n++)
+		if (-1 < curbp->b_point - n && (utf8_size(*(ptr(curbp, curbp->b_point - n))) == n))
+			return n;
+	return 1;
+}
+
+void lnend(void)
+{
+        if (curbp->b_point == pos(curbp, curbp->b_ebuf)) return; /* do nothing if EOF */
+	curbp->b_point = dndn(curbp, curbp->b_point);
+	point_t p = curbp->b_point;
+	left();
+	curbp->b_point = (*ptr(curbp, curbp->b_point) == '\n') ? curbp->b_point : p;
+}
+
+void wleft(void)
+{
+	char_t *p;
+	while (!isspace(*(p = ptr(curbp, curbp->b_point))) && curbp->b_buf < p)
+		--curbp->b_point;
+	while (isspace(*(p = ptr(curbp, curbp->b_point))) && curbp->b_buf < p)
+		--curbp->b_point;
+}
+
+void pgdown(void)
+{
+	curbp->b_page = curbp->b_point = upup(curbp, curbp->b_epage);
+	while (0 < curbp->b_row--)
+		down();
+	curbp->b_epage = pos(curbp, curbp->b_ebuf);
+}
+
+void pgup(void)
+{
+	int i = curwp->w_rows;
+	while (0 < --i) {
+		curbp->b_page = upup(curbp, curbp->b_page);
+		up();
+	}
+}
+
+void wright(void)
+{
+	char_t *p;
+	while (!isspace(*(p = ptr(curbp, curbp->b_point))) && p < curbp->b_ebuf)
+		++curbp->b_point;
+	while (isspace(*(p = ptr(curbp, curbp->b_point))) && p < curbp->b_ebuf)
+		++curbp->b_point;
+}
+
+void insert(void)
+{
+	assert(curbp->b_gap <= curbp->b_egap);
+	if (curbp->b_gap == curbp->b_egap && !growgap(curbp, CHUNK))
+		return;
+	curbp->b_point = movegap(curbp, curbp->b_point);
+
+	/* overwrite if mid line, not EOL or EOF, CR will insert as normal */
+	if ((curbp->b_flags & B_OVERWRITE) && *input != '\r' && *(ptr(curbp, curbp->b_point)) != '\n' && curbp->b_point < pos(curbp,curbp->b_ebuf) ) {
+		*(ptr(curbp, curbp->b_point)) = *input;
+		if (curbp->b_point < pos(curbp, curbp->b_ebuf))
+			++curbp->b_point;
+	} else {
+		*curbp->b_gap++ = *input == '\r' ? '\n' : *input;
+		curbp->b_point = pos(curbp, curbp->b_egap);
+		// force reframe if scrolled off bottom of screen and at EOF
+		if (curbp->b_point == pos(curbp, curbp->b_ebuf) && curbp->b_point >= curbp->b_epage) curbp->b_reframe = 1;
+	}
+	curbp->b_flags |= B_MODIFIED;
+}
+
+void backsp(void)
+{
+	curbp->b_point = movegap(curbp, curbp->b_point);
+	if (curbp->b_buf < curbp->b_gap) {
+		curbp->b_gap -= prev_utf8_char_size();
+		curbp->b_flags |= B_MODIFIED;
+	}
+	curbp->b_point = pos(curbp, curbp->b_egap);
+}
+
+void delete(void)
+{
+	curbp->b_point = movegap(curbp, curbp->b_point);
+	if (curbp->b_egap < curbp->b_ebuf) {
+		curbp->b_egap += utf8_size(*curbp->b_egap);
+		curbp->b_point = pos(curbp, curbp->b_egap);
+		curbp->b_flags |= B_MODIFIED;
+	}
+}
+
+void gotoline(void)
+{
+	long lline;
+	int line;
+	char *ep;
+	point_t p;
+
+	if (getinput("Goto line: ", temp, STRBUF_S, F_CLEAR)) {
+		errno = 0;
+		lline = strtol(temp, &ep, 10);
+		if (ep == temp || *ep != '\0' || errno != 0 || lline < INT_MIN || INT_MAX < lline) {
+			msg("Line %s, not valid", temp);
+			return;
+		}
+		line = (int) lline;
+		p = line_to_point(line);
+		if (p != -1) {
+			curbp->b_point = p;
+			msg("Line %d", line);
+		} else {
+			msg("Line %d, not found", line);
+		}
+	}
+}
+
+void insertfile(void)
+{
+	if (getfilename("Insert file: ", temp, NAME_MAX))
+		(void)insert_file(temp, TRUE);
+}
+
+void readfile(void)
+{
+	buffer_t *bp;
+	
+	temp[0] = '\0';
+	int result = getfilename("Find file: ", (char*)temp, NAME_MAX);
+	/* int result = getinput("Find file: ", (char*)temp, NAME_MAX, F_CLEAR); */
+
+	if (result) {
+		bp = find_buffer(temp, TRUE);
+		disassociate_b(curwp); /* we are leaving the old buffer for a new one */
+		curbp = bp;
+		associate_b2w(curbp, curwp);
+
+		/* load the file if not already loaded */
+		if (bp != NULL && bp->b_fname[0] == '\0') {
+			if (!load_file(temp)) {
+				msg("New file %s", temp);
+			}
+			strlcpy(curbp->b_fname, temp, NAME_MAX);
+		}
+	}
+}
+
+void savebuffer(void)
+{
+	if (curbp->b_fname[0] != '\0') {
+		save(curbp->b_fname);
+		return;
+	} else {
+		writefile();
+	}
+	refresh();
+}
+
+void writefile(void)
+{
+	strlcpy(temp, curbp->b_fname, NAME_MAX);
+	if (getinput("Write file: ", temp, NAME_MAX, F_NONE))
+		if (save(temp) == TRUE)
+			strlcpy(curbp->b_fname, temp, NAME_MAX);
+}
+
+void killbuffer(void)
+{
+	buffer_t *kill_bp = curbp;
+	buffer_t *bp;
+	int bcount = count_buffers();
+
+	/* do nothing if only buffer left is the scratch buffer */
+	if (bcount == 1 && 0 == strcmp(get_buffer_name(curbp), "*scratch*"))
+		return;
+	
+	if (curbp->b_flags & B_MODIFIED) {
+		mvaddstr(MSGLINE, 0, "Discard changes (y/n) ?");
+		clrtoeol();
+		if (!yesno(FALSE))
+			return;
+	}
+
+	if (bcount == 1) {
+		/* create a scratch buffer */
+		bp = find_buffer("*scratch*", TRUE);
+		strcpy(bp->b_bname, "*scratch*");
+	}
+
+	next_buffer();
+	assert(kill_bp != curbp);
+	delete_buffer(kill_bp);
+}
+
+void iblock(void)
+{
+	block();
+	msg("Mark set");
+}
+
+void toggle_overwrite_mode(void) {
+	if (curbp->b_flags & B_OVERWRITE)
+		curbp->b_flags &= ~B_OVERWRITE;
+	else
+		curbp->b_flags |= B_OVERWRITE;
+}
+
+void killtoeol(void)
+{
+        if (curbp->b_point == pos(curbp, curbp->b_ebuf))
+		return; /* do nothing if at end of file */
+	if (*(ptr(curbp, curbp->b_point)) == 0xa) {
+		delete(); /* delete CR if at start of empty line */
+	} else {
+		curbp->b_mark = curbp->b_point;
+		lnend();
+		if (curbp->b_mark != curbp->b_point) copy_cut(TRUE);
+	}
+}
+
+void copy_cut(int cut)
+{
+	char_t *p;
+	/* if no mark or point == marker, nothing doing */
+	if (curbp->b_mark == NOMARK || curbp->b_point == curbp->b_mark)
+		return;
+	if (scrap != NULL) {
+		free(scrap);
+		scrap = NULL;
+	}
+	if (curbp->b_point < curbp->b_mark) {
+		/* point above marker: move gap under point, region = marker - point */
+		(void) movegap(curbp, curbp->b_point);
+		p = ptr(curbp, curbp->b_point);
+		nscrap = curbp->b_mark - curbp->b_point;
+	} else {
+		/* if point below marker: move gap under marker, region = point - marker */
+		(void) movegap(curbp, curbp->b_mark);
+		p = ptr(curbp, curbp->b_mark);
+		nscrap = curbp->b_point - curbp->b_mark;
+	}
+	if ((scrap = (char_t*) malloc(nscrap)) == NULL) {
+		msg("No more memory available.");
+	} else {
+		(void) memcpy(scrap, p, nscrap * sizeof (char_t));
+		if (cut) {
+			curbp->b_egap += nscrap; /* if cut expand gap down */
+			curbp->b_point = pos(curbp, curbp->b_egap); /* set point to after region */
+			curbp->b_flags |= B_MODIFIED;
+			msg("%ld bytes cut.", nscrap);
+		} else {
+			msg("%ld bytes copied.", nscrap);
+		}
+		curbp->b_mark = NOMARK;  /* unmark */
+	}
+}
+
+void paste(void)
+{
+	if(curbp->b_flags & B_OVERWRITE)
+		return;
+	if (nscrap <= 0) {
+		msg("Scrap is empty.  Nothing to paste.");
+	} else if (nscrap < curbp->b_egap - curbp->b_gap || growgap(curbp, nscrap)) {
+		curbp->b_point = movegap(curbp, curbp->b_point);
+		memcpy(curbp->b_gap, scrap, nscrap * sizeof (char_t));
+		curbp->b_gap += nscrap;
+		curbp->b_point = pos(curbp, curbp->b_egap);
+		curbp->b_flags |= B_MODIFIED;
+	}
+}
+
+void showpos(void)
+{
+	int current, lastln;
+	point_t end_p = pos(curbp, curbp->b_ebuf);
+    
+	get_line_stats(&current, &lastln);
+
+	if (curbp->b_point == end_p) {
+		msg("[EOB] Line = %d/%d  Point = %d/%d", current, lastln,
+			curbp->b_point, ((curbp->b_ebuf - curbp->b_buf) - (curbp->b_egap - curbp->b_gap)));
+	} else {
+		msg("Char = %s 0x%x  Line = %d/%d  Point = %d/%d", unctrl(*(ptr(curbp, curbp->b_point))), *(ptr(curbp, curbp->b_point)), 
+			current, lastln,
+			curbp->b_point, ((curbp->b_ebuf - curbp->b_buf) - (curbp->b_egap - curbp->b_gap)));
+	}
+}
Index: usr.bin/atto/complete.c
===================================================================
RCS file: usr.bin/atto/complete.c
diff -N usr.bin/atto/complete.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/complete.c	16 Oct 2026 11:30:03 -0000
@@ -0,0 +1,86 @@
+/* complete.c, Atto Emacs, Hugh Barney, Public Domain, 2016 */
+
+#include "header.h"
+
+/* basic filename completion, based on code in uemacs/PK */
+int getfilename(const char *prompt, char *buf, int nbuf)
+{
+	static char temp_file[] = TEMPFILE;
+	int cpos = 0;	/* current character position in string */
+	int k = 0, c, fd, didtry, iswild = 0;
+
+	char sys_command[255];
+	FILE *fp = NULL;
+	buf[0] ='\0';
+
+	for (;;) {
+		didtry = (k == 0x09);	/* Was last command tab-completion? */
+		display_prompt_and_response(prompt, buf);
+		k = getch(); /* get a character from the user */
+
+		switch(k) {
+		case 0x0a: /* cr, lf */
+		case 0x0d:
+			if (cpos > 0 && NULL != strchr(buf, '~')) goto do_tab;
+			/* FALLTHRU */
+		case 0x07: /* ctrl-g, abort */
+			if (fp != NULL) fclose(fp);
+			return (k != 0x07 && cpos > 0);
+
+		case 0x7f: /* del, erase */
+		case 0x08: /* backspace */
+			if (cpos == 0) continue;
+			buf[--cpos] = '\0';
+			break;
+
+		case  0x15: /* C-u kill */
+			cpos = 0;
+			buf[0] = '\0';
+			break;
+
+do_tab: 
+		case 0x09: /* TAB, complete file name */
+			/* scan backwards for a wild card and set */
+			iswild=0;
+			while (cpos > 0) {
+				cpos--;
+				if (buf[cpos] == '*' || buf[cpos] == '?')
+					iswild = 1;
+			}
+
+			/* first time retrieval */
+			if (! didtry) {
+				if (fp != NULL) fclose(fp);
+				strcpy(temp_file, TEMPFILE);
+				if (-1 == (fd = mkstemp(temp_file)))
+					fatal("%s: Failed to create temp file\n");
+				strcpy(sys_command, "echo ");
+				strcat(sys_command, buf);
+				if (!iswild) strcat(sys_command, "*");
+				strcat(sys_command, " >");
+				strcat(sys_command, temp_file);
+				strcat(sys_command, " 2>&1");
+				(void) system(sys_command); /* stop compiler unused result warning */
+				fp = fdopen(fd, "r");
+				unlink(temp_file);
+			}
+
+			/* copy next filename into buf */
+			while ((c = getc(fp)) != EOF && c != '\n' && c != ' ')
+				if (cpos < nbuf - 1 && c != '*')
+					buf[cpos++] = c;
+
+			buf[cpos] = '\0';
+			if (c != ' ') rewind(fp);
+			didtry = 1;
+			break;
+
+		default:
+			if (cpos < nbuf - 1) {
+				  buf[cpos++] = k;
+				  buf[cpos] = '\0';
+			}
+			break;
+		}
+	}
+}
Index: usr.bin/atto/display.c
===================================================================
RCS file: usr.bin/atto/display.c
diff -N usr.bin/atto/display.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/display.c	16 Oct 2026 11:30:03 -0000
@@ -0,0 +1,296 @@
+/* display.c, Atto Emacs, Public Domain, Hugh Barney, 2016, Derived from: Anthony's Editor January 93 */
+
+#include "header.h"
+
+/* Reverse scan for start of logical line containing offset */
+point_t lnstart(buffer_t *bp, point_t off)
+{
+	char_t *p;
+	do
+		p = ptr(bp, --off);
+	while (bp->b_buf < p && *p != '\n');
+	return (bp->b_buf < p ? ++off : 0);
+}
+
+/* Forward scan for start of logical line segment (corresponds to screen line)  containing 'finish' */
+point_t segstart(buffer_t *bp, point_t start, point_t finish)
+{
+	char_t *p;
+	int c = 0;
+	point_t scan = start;
+
+	while (scan < finish) {
+		p = ptr(bp, scan);
+		if (*p == '\n') {
+			c = 0;
+			start = scan + 1;
+		} else if (COLS <= c) {
+			c = 0;
+			start = scan;
+		}
+		scan += utf8_size(*ptr(bp,scan));
+		c += *p == '\t' ? 8 - (c & 7) : 1;
+	}
+	return (c < COLS ? start : finish);
+}
+
+/* Forward scan for start of logical line segment following 'finish' */
+point_t segnext(buffer_t *bp, point_t start, point_t finish)
+{
+	char_t *p;
+	int c = 0;
+
+	point_t scan = segstart(bp, start, finish);
+	for (;;) {
+		p = ptr(bp, scan);
+		if (bp->b_ebuf <= p || COLS <= c)
+			break;
+		scan += utf8_size(*ptr(bp,scan));
+		if (*p == '\n')
+			break;
+		c += *p == '\t' ? 8 - (c & 7) : 1;
+	}
+	return (p < bp->b_ebuf ? scan : pos(bp, bp->b_ebuf));
+}
+
+/* Move up one screen line */
+point_t upup(buffer_t *bp, point_t off)
+{
+	point_t curr = lnstart(bp, off);
+	point_t seg = segstart(bp, curr, off);
+	if (curr < seg)
+		off = segstart(bp, curr, seg-1);
+	else
+		off = segstart(bp, lnstart(bp,curr-1), curr-1);
+	return (off);
+}
+
+/* Move down one screen line */
+point_t dndn(buffer_t *bp, point_t off)
+{
+	return (segnext(bp, lnstart(bp,off), off));
+}
+
+/* Return the offset of a column on the specified line */
+point_t lncolumn(buffer_t *bp, point_t offset, int column)
+{
+	int c = 0;
+	char_t *p;
+	while ((p = ptr(bp, offset)) < bp->b_ebuf && *p != '\n' && c < column) {
+		c += *p == '\t' ? 8 - (c & 7) : 1;
+		offset += utf8_size(*ptr(bp,offset));
+	}
+	return (offset);
+}
+
+void display(window_t *wp, int flag)
+{
+	char_t *p;
+	int i, j, k, nch;
+	buffer_t *bp = wp->w_bufp;
+	int token_type = ID_DEFAULT;
+	
+	/* find start of screen, handle scroll up off page or top of file  */
+	/* point is always within b_page and b_epage */
+	if (bp->b_point < bp->b_page)
+		bp->b_page = segstart(bp, lnstart(bp,bp->b_point), bp->b_point);
+
+	/* reframe when scrolled off bottom */
+	if (bp->b_reframe == 1 || (bp->b_epage <= bp->b_point && curbp->b_point != pos(curbp, curbp->b_ebuf))) {
+		bp->b_reframe = 0;
+		/* Find end of screen plus one. */
+		bp->b_page = dndn(bp, bp->b_point);
+		/* if we scroll to EOF we show 1 blank line at bottom of screen */
+		if (pos(bp, bp->b_ebuf) <= bp->b_page) {
+			bp->b_page = pos(bp, bp->b_ebuf);
+			i = wp->w_rows - 1;
+		} else {
+			i = wp->w_rows - 0;
+		}
+		/* Scan backwards the required number of lines. */
+		while (0 < i--)
+			bp->b_page = upup(bp, bp->b_page);
+	}
+
+	move(wp->w_top, 0); /* start from top of window */
+	i = wp->w_top;
+	j = 0;
+	bp->b_epage = bp->b_page;
+
+
+	if (use_color) {
+		set_parse_state(bp, bp->b_epage); /* are we in a multline comment ? */
+	} else {
+		attron(COLOR_PAIR(ID_DEFAULT));
+	}
+
+	/* paint screen from top of page until we hit maxline */ 
+	while (1) {
+		/* reached point - store the cursor position */
+		if (bp->b_point == bp->b_epage) {
+			bp->b_row = i;
+			bp->b_col = j;
+		}
+		p = ptr(bp, bp->b_epage);
+		nch = 1;
+		if (wp->w_top + wp->w_rows <= i || bp->b_ebuf <= p) /* maxline */
+			break;
+		if (*p != '\r') {
+			nch = utf8_size(*p);
+			if ( nch > 1) {
+				wchar_t c;
+				/* reset if invalid multi-byte character */
+				if (mbtowc(&c, (char*)p, 6) < 0) mbtowc(NULL, NULL, 0); 
+				j += wcwidth(c) < 0 ? 1 : wcwidth(c);
+				display_utf8(bp, nch);
+			} else if (isprint(*p) || *p == '\t' || *p == '\n') {
+				j += *p == '\t' ? 8-(j&7) : 1;
+				if (use_color) {
+					token_type = parse_text(bp, bp->b_epage);
+					attron(COLOR_PAIR(token_type));
+				}
+				addch(*p);
+			} else {
+				const char *ctrl = unctrl(*p);
+				j += (int) strlen(ctrl);
+				addstr(ctrl);
+			}
+		}
+		if (*p == '\n' || COLS <= j) {
+			j -= COLS;
+			if (j < 0)
+				j = 0;
+			++i;
+		}
+		bp->b_epage = bp->b_epage + nch;
+	}
+
+	/* replacement for clrtobot() to bottom of window */
+	for (k=i; k < wp->w_top + wp->w_rows; k++) {
+		move(k, j); /* clear from very last char not start of line */
+		clrtoeol();
+		j = 0; /* thereafter start of line */
+	}
+
+	b2w(wp); /* save buffer stuff on window */
+	modeline(wp);
+	if (wp == curwp && flag) {
+		dispmsg();
+		move(bp->b_row, bp->b_col); /* set cursor */
+		refresh();
+	}
+	wp->w_update = FALSE;
+
+	if (use_color) {
+		attron(COLOR_PAIR(ID_SYMBOL));
+	}
+}
+
+void display_utf8(buffer_t *bp, int n)
+{
+	char sbuf[6];
+	int i = 0;
+
+	for (i=0; i<n; i++)
+		sbuf[i] = *ptr(bp, bp->b_epage + i);
+	sbuf[n] = '\0';
+	addstr(sbuf);
+}
+
+void modeline(window_t *wp)
+{
+	int i;
+	char lch, mch, och;
+	
+	attron(COLOR_PAIR(ID_MODELINE));
+	move(wp->w_top + wp->w_rows, 0);
+	lch = (wp == curwp ? '=' : '-');
+	mch = ((wp->w_bufp->b_flags & B_MODIFIED) ? '*' : lch);
+	och = ((wp->w_bufp->b_flags & B_OVERWRITE) ? 'O' : lch);
+
+	sprintf(temp, "%c%c%c Atto: %c%c %s",  lch,och,mch,lch,lch, get_buffer_name(wp->w_bufp));
+	addstr(temp);
+
+	for (i = strlen(temp) + 1; i <= COLS; i++)
+		addch(lch);
+}
+
+void dispmsg(void)
+{
+	move(MSGLINE, 0);
+	if (msgflag) {
+		attron(COLOR_PAIR(ID_SYMBOL));
+		addstr(msgline);
+		msgflag = FALSE;
+	}
+	clrtoeol();
+}
+
+void display_prompt_and_response(const char *prompt, const char *response)
+{
+	mvaddstr(MSGLINE, 0, prompt);
+	/* if we have a value print it and go to end of it */
+	if (response[0] != '\0')
+		addstr(response);
+	clrtoeol();
+}
+
+void update_display(void)
+{   
+	window_t *wp;
+	buffer_t *bp;
+
+	bp = curwp->w_bufp;
+	bp->b_cpoint = bp->b_point; /* cpoint only ever set here */
+	
+	/* only one window */
+	if (wheadp->w_next == NULL) {
+		display(curwp, TRUE);
+		refresh();
+		bp->b_psize = bp->b_size;
+		return;
+	}
+
+	display(curwp, FALSE); /* this is key, we must call our win first to get accurate page and epage etc */
+	
+	/* never curwp,  but same buffer in different window or update flag set*/
+	for (wp=wheadp; wp != NULL; wp = wp->w_next) {
+		if (wp != curwp && (wp->w_bufp == bp || wp->w_update)) {
+			w2b(wp);
+			display(wp, FALSE);
+		}
+	}
+
+	/* now display our window and buffer */
+	w2b(curwp);
+	dispmsg();
+	move(curwp->w_row, curwp->w_col); /* set cursor for curwp */
+	refresh();
+	bp->b_psize = bp->b_size;  /* now safe to save previous size for next time */
+}
+
+void w2b(window_t *w)
+{
+	w->w_bufp->b_point = w->w_point;
+	w->w_bufp->b_page = w->w_page;
+	w->w_bufp->b_epage = w->w_epage;
+	w->w_bufp->b_row = w->w_row;
+	w->w_bufp->b_col = w->w_col;
+	
+	/* fixup pointers in other windows of the same buffer, if size of edit text changed */
+	if (w->w_bufp->b_point > w->w_bufp->b_cpoint) {
+		w->w_bufp->b_point += (w->w_bufp->b_size - w->w_bufp->b_psize);
+		w->w_bufp->b_page += (w->w_bufp->b_size - w->w_bufp->b_psize);
+		w->w_bufp->b_epage += (w->w_bufp->b_size - w->w_bufp->b_psize);
+	}
+}
+
+void b2w(window_t *w)
+{
+	w->w_point = w->w_bufp->b_point;
+	w->w_page = w->w_bufp->b_page;
+	w->w_epage = w->w_bufp->b_epage;
+	w->w_row = w->w_bufp->b_row;
+	w->w_col = w->w_bufp->b_col;
+	w->w_bufp->b_size = (w->w_bufp->b_ebuf - w->w_bufp->b_buf) - (w->w_bufp->b_egap - w->w_bufp->b_gap);
+}
Index: usr.bin/atto/gap.c
===================================================================
RCS file: usr.bin/atto/gap.c
diff -N usr.bin/atto/gap.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/gap.c	16 Oct 2026 11:30:03 -0000
@@ -0,0 +1,210 @@
+/* gap.c, Atto Emacs, Public Domain, Hugh Barney, 2016, Derived from: Anthony's Editor January 93 */
+
+#include <sys/stat.h>
+#include "header.h"
+
+/* Enlarge gap by n chars, position of gap cannot change */
+int growgap(buffer_t *bp, point_t n)
+{
+	char_t *new;
+	point_t buflen, newlen, xgap, xegap;
+		
+	assert(bp->b_buf <= bp->b_gap);
+	assert(bp->b_gap <= bp->b_egap);
+	assert(bp->b_egap <= bp->b_ebuf);
+
+	xgap = bp->b_gap - bp->b_buf;
+	xegap = bp->b_egap - bp->b_buf;
+	buflen = bp->b_ebuf - bp->b_buf;
+    
+	/* reduce number of reallocs by growing by a minimum amount */
+	n = (n < MIN_GAP_EXPAND ? MIN_GAP_EXPAND : n);
+	newlen = buflen + n * sizeof (char_t);
+
+	if (buflen == 0) {
+		if (newlen < 0 || MAX_SIZE_T < (size_t) newlen)
+			fatal("%s: Failed to allocate required memory.\n");
+		new = (char_t*) malloc((size_t) newlen);
+		if (new == NULL)			
+			fatal("%s: Failed to allocate required memory.\n");	/* Cannot edit a file without a buffer. */
+	} else {
+		if (newlen < 0 || MAX_SIZE_T < (size_t) newlen) {
+			msg("Failed to allocate required memory");
+			return (FALSE);
+		}
+		new = (char_t*) realloc(bp->b_buf, (size_t) newlen);
+		if (new == NULL) {
+			msg("Failed to allocate required memory");    /* Report non-fatal error. */
+			return (FALSE);
+		}
+	}
+
+	/* Relocate pointers in new buffer and append the new
+	 * extension to the end of the gap.
+	 */
+	bp->b_buf = new;
+	bp->b_gap = bp->b_buf + xgap;      
+	bp->b_ebuf = bp->b_buf + buflen;
+	bp->b_egap = bp->b_buf + newlen;
+	while (xegap < buflen--)
+		*--bp->b_egap = *--bp->b_ebuf;
+	bp->b_ebuf = bp->b_buf + newlen;
+
+	assert(bp->b_buf < bp->b_ebuf);          /* Buffer must exist. */
+	assert(bp->b_buf <= bp->b_gap);
+	assert(bp->b_gap < bp->b_egap);          /* Gap must grow only. */
+	assert(bp->b_egap <= bp->b_ebuf);
+	return (TRUE);
+}
+
+point_t movegap(buffer_t *bp, point_t offset)
+{
+	char_t *p = ptr(bp, offset);
+	while (p < bp->b_gap)
+		*--bp->b_egap = *--bp->b_gap;
+	while (bp->b_egap < p)
+		*bp->b_gap++ = *bp->b_egap++;
+	assert(bp->b_gap <= bp->b_egap);
+	assert(bp->b_buf <= bp->b_gap);
+	assert(bp->b_egap <= bp->b_ebuf);
+	return (pos(bp, bp->b_egap));
+}
+
+/* Given a buffer offset, convert it to a pointer into the buffer */
+char_t * ptr(buffer_t *bp, point_t offset)
+{
+	if (offset < 0)
+		return (bp->b_buf);
+	return (bp->b_buf+offset + (bp->b_buf + offset < bp->b_gap ? 0 : bp->b_egap-bp->b_gap));
+}
+
+/* Given a pointer into the buffer, convert it to a buffer offset */
+point_t pos(buffer_t *bp, char_t *cp)
+{
+	assert(bp->b_buf <= cp && cp <= bp->b_ebuf);
+	return (cp - bp->b_buf - (cp < bp->b_egap ? 0 : bp->b_egap - bp->b_gap));
+}
+
+int posix_file(char *fn)
+{
+	if (fn[0] == '_')
+		return (FALSE);
+
+	for (; *fn != '\0'; ++fn) {
+		if (!isalnum((unsigned char)*fn) && *fn != '.' && *fn != '_' && *fn != '-' && *fn != '/')
+			return (FALSE);
+	}
+	return (TRUE);
+}
+
+int save(char *fn)
+{
+	FILE *fp;
+	point_t length;
+		
+	if (!posix_file(fn)) {
+		msg("Not a portable POSIX file name.");
+		return (FALSE);
+	}
+	fp = fopen(fn, "w");
+	if (fp == NULL) {
+		msg("Failed to open file \"%s\".", fn);
+		return (FALSE);
+	}
+	(void) movegap(curbp, (point_t) 0);
+	length = (point_t) (curbp->b_ebuf - curbp->b_egap);
+	if (fwrite(curbp->b_egap, sizeof (char), (size_t) length, fp) != (size_t)length) {
+		msg("Failed to write file \"%s\".", fn);
+		return (FALSE);
+	}
+	if (fclose(fp) != 0) {
+		msg("Failed to close file \"%s\".", fn);
+		return (FALSE);
+	}
+	curbp->b_flags &= ~B_MODIFIED;
+	msg("File \"%s\" %ld bytes saved.", fn, pos(curbp, curbp->b_ebuf));
+	return (TRUE);
+}
+
+int load_file(char *fn)
+{
+	/* reset the gap, make it the whole buffer */
+	curbp->b_gap = curbp->b_buf;
+	curbp->b_egap = curbp->b_ebuf;
+	top();
+	return insert_file(fn, FALSE);
+}
+
+/* reads file into buffer at point */
+int insert_file(char *fn, int modflag)
+{
+	FILE *fp;
+	size_t len;
+	struct stat sb;
+
+	if (stat(fn, &sb) < 0) {
+		msg("Failed to find file \"%s\".", fn);
+		return (FALSE);
+	}
+	if (MAX_SIZE_T < (size_t)sb.st_size) {
+		msg("File \"%s\" is too big to load.", fn);
+		return (FALSE);
+	}
+	if (curbp->b_egap - curbp->b_gap < sb.st_size && !growgap(curbp, sb.st_size))
+		return (FALSE);
+	if ((fp = fopen(fn, "r")) == NULL) {
+		msg("Failed to open file \"%s\".", fn);
+		return (FALSE);
+	}
+	curbp->b_point = movegap(curbp, curbp->b_point);
+	curbp->b_gap += len = fread(curbp->b_gap, sizeof (char), (size_t) sb.st_size, fp);
+
+	if (fclose(fp) != 0) {
+		msg("Failed to close file \"%s\".", fn);
+		return (FALSE);
+	}
+	curbp->b_flags &= (modflag ? B_MODIFIED : ~B_MODIFIED);
+	msg("File \"%s\" %ld bytes read.", fn, len);
+	return (TRUE);
+}
+
+/* find the point for start of line ln */
+point_t line_to_point(int ln)
+{
+	point_t end_p = pos(curbp, curbp->b_ebuf);
+	point_t p, start;
+
+	for (p=0, start=0; p < end_p; p++) {
+		if ( *(ptr(curbp, p)) == '\n') {
+			if (--ln == 0)
+				return start;
+			if (p + 1 < end_p) 
+				start = p + 1;
+		}
+	}
+	return -1;
+}
+
+/* scan buffer and fill in curline and lastline */
+void get_line_stats(int *curline, int *lastline)
+{
+	point_t end_p = pos(curbp, curbp->b_ebuf);
+	point_t p;
+	int line;
+    
+	*curline = -1;
+    
+	for (p=0, line=0; p < end_p; p++) {
+		line += (*(ptr(curbp,p)) == '\n') ? 1 : 0;
+		*lastline = line;
+        
+		if (*curline == -1 && p == curbp->b_point) {
+			*curline = (*(ptr(curbp,p)) == '\n') ? line : line + 1;
+		}
+	}
+
+	*lastline = *lastline + 1;
+	
+	if (curbp->b_point == end_p)
+		*curline = *lastline;
+}
Index: usr.bin/atto/header.h
===================================================================
RCS file: usr.bin/atto/header.h
diff -N usr.bin/atto/header.h
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/header.h	16 Oct 2026 11:30:03 -0000
@@ -0,0 +1,212 @@
+/* header.h, Atto Emacs, Public Domain, Hugh Barney, 2016, Derived from: Anthony's Editor January 93 */
+#ifndef _NETBSD_SOURCE
+#define _NETBSD_SOURCE
+#endif
+#define _XOPEN_SOURCE 600
+#include <locale.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <assert.h>
+#include <errno.h>
+#include <curses.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <ctype.h>
+#include <limits.h>
+#include <string.h>
+#include <unistd.h>
+#include <wchar.h>
+
+#define VERSION	 "Atto 1.22, Public Domain, Dec 2020, by Hugh Barney,  No warranty."
+#define PROG_NAME "atto"
+#define B_MODIFIED	0x01		/* modified buffer */
+#define B_OVERWRITE	0x02		/* overwrite mode */
+#define MSGLINE         (LINES-1)
+#define NOMARK          -1
+#define CHUNK           8096L
+#define K_BUFFER_LENGTH 256
+#define TEMPBUF         512
+#define STRBUF_L        256
+#define STRBUF_M        64
+#define STRBUF_S        16
+#define MIN_GAP_EXPAND  512
+#define TEMPFILE        "/tmp/feXXXXXX"
+#define F_NONE          0
+#define F_CLEAR         1
+#define ID_DEFAULT         1
+#define ID_SYMBOL          2
+#define ID_MODELINE        3
+#define ID_DIGITS          4
+#define ID_LINE_COMMENT    5
+#define ID_BLOCK_COMMENT   6
+#define ID_DOUBLE_STRING   7
+#define ID_SINGLE_STRING   8
+
+typedef unsigned char char_t;
+typedef long point_t;
+
+typedef struct keymap_t {
+	const char *key_desc;   	/* name of bound function */
+	const char *key_bytes;  	/* the string of bytes when this key is pressed */
+	void (*func)(void);
+} keymap_t;
+
+typedef struct buffer_t
+{
+	struct buffer_t *b_next;  /* Link to next buffer_t */
+	point_t b_mark;	     	  /* the mark */
+	point_t b_point;          /* the point */
+	point_t b_cpoint;         /* the original current point, used for multiple window displaying */
+	point_t b_page;           /* start of page */
+	point_t b_epage;          /* end of page */
+	point_t b_reframe;        /* force a reframe of the display */
+	int b_cnt;                /* count of windows referencing this buffer */
+	int b_size;               /* current size of text being edited (not including gap) */
+	int b_psize;              /* previous size */
+	char_t *b_buf;            /* start of buffer */
+	char_t *b_ebuf;           /* end of buffer */
+	char_t *b_gap;            /* start of gap */
+	char_t *b_egap;           /* end of gap */
+	int b_row;                /* cursor row */
+	int b_col;                /* cursor col */
+	char b_fname[NAME_MAX + 1]; /* filename */
+	char b_bname[STRBUF_S];   /* buffer name */
+	char b_flags;             /* buffer flags */
+} buffer_t;
+
+typedef struct window_t
+{
+	struct window_t *w_next;   /* Next window */
+	struct buffer_t *w_bufp;   /* Buffer displayed in window */
+	point_t w_point;
+	point_t w_mark;
+	point_t w_page;
+	point_t w_epage;
+	char w_top;	    /* Origin 0 top row of window */
+	char w_rows;        /* no. of rows of text in window */
+	int w_row;          /* cursor row */
+	int w_col;          /* cursor col */
+	int w_update;
+	char w_name[STRBUF_S];
+} window_t;
+
+extern buffer_t *curbp;			/* current buffer */
+extern buffer_t *bheadp;		/* head of list of buffers */
+extern window_t *curwp;
+extern window_t *wheadp;
+
+/*
+ * Some compilers define size_t as a unsigned 16 bit number while
+ * point_t and off_t might be defined as a signed 32 bit number.  
+ * malloc(), realloc(), fread(), and fwrite() take size_t parameters,
+ * which means there will be some size limits because size_t is too
+ * small of a type.
+ */
+#define MAX_SIZE_T      ((unsigned long) (size_t) ~0)
+
+extern int done;                /* Quit flag. */
+extern int use_color;           /* Optional syntax highlighting. */
+extern int msgflag;             /* True if msgline should be displayed. */
+extern point_t nscrap;          /* Length of scrap buffer. */
+extern char_t *scrap;           /* Allocated scrap buffer. */
+extern char_t *input;
+extern char msgline[];          /* Message line input/output buffer. */
+extern char temp[];             /* Temporary buffer. */
+extern char searchtext[];
+extern char replace[];
+extern keymap_t *key_map;       /* Command key mappings. */
+extern keymap_t keymap[];
+extern keymap_t *key_return;    /* Command key return */
+
+extern void fatal(const char *);
+extern void msg(const char *, ...);
+extern void display(window_t *, int);
+extern void dispmsg(void);
+extern void modeline(window_t *);
+extern int utf8_size(char_t);
+extern int prev_utf8_char_size(void);
+extern void display_utf8(buffer_t *, int);
+extern point_t lnstart(buffer_t *, point_t);
+extern point_t lncolumn(buffer_t *, point_t, int);
+extern point_t segstart(buffer_t *, point_t, point_t);
+extern point_t segnext(buffer_t *, point_t, point_t);
+extern point_t upup(buffer_t *, point_t);
+extern point_t dndn(buffer_t *, point_t);
+extern char_t *get_key(keymap_t *, keymap_t **);
+extern int getinput(const char *, char *, int, int);
+extern int getfilename(const char *, char *, int);
+extern void display_prompt_and_response(const char *, const char *);
+extern int growgap(buffer_t *, point_t);
+extern point_t movegap(buffer_t *, point_t);
+extern point_t pos(buffer_t *, char_t *);
+extern char_t *ptr(buffer_t *, point_t);
+extern int posix_file(char *);
+extern int save(char *);
+extern int load_file(char *);
+extern int insert_file(char *, int);
+extern void backsp(void);
+extern void block(void);
+extern void iblock(void);
+extern void bottom(void);
+extern void cut(void);
+extern void copy(void);
+extern void copy_cut(int);
+extern void delete(void);
+extern void toggle_overwrite_mode(void);
+extern void down(void);
+extern void insert(void);
+extern void left(void);
+extern void lnbegin(void);
+extern void lnend(void);
+extern void paste(void);
+extern void pgdown(void);
+extern void pgup(void);
+extern void quit(void);
+extern int yesno(int);
+extern void quit_ask(void);
+extern void redraw(void);
+extern void readfile(void);
+extern void insertfile(void);
+extern void right(void);
+extern void top(void);
+extern void up(void);
+extern void version(void);
+extern void wleft(void);
+extern void wright(void);
+extern void writefile(void);
+extern void savebuffer(void);
+extern void showpos(void);
+extern void killtoeol(void);
+extern void gotoline(void);
+extern void search(void);
+extern void query_replace(void);
+extern point_t line_to_point(int);
+extern point_t search_forward(buffer_t *, point_t, char *);
+extern point_t search_backwards(buffer_t *, point_t, char *);
+extern void update_search_prompt(char *, char *);
+extern void display_search_result(point_t, int, const char *, const char *);
+extern buffer_t* find_buffer(const char *, int);
+extern void buffer_init(buffer_t *);
+extern int delete_buffer(buffer_t *);
+extern void next_buffer(void);
+extern int count_buffers(void);
+extern int modified_buffers(void);
+extern void killbuffer(void);
+extern char* get_buffer_name(buffer_t *);
+extern void get_line_stats(int *, int *);
+extern void query_replace(void);
+extern window_t *new_window(void);
+extern void one_window(window_t *);
+extern void split_window(void);
+extern void next_window(void);
+extern void delete_other_windows(void);
+extern void free_other_windows(window_t *);
+extern void update_display(void);
+extern void w2b(window_t *);
+extern void b2w(window_t *);
+extern void associate_b2w(buffer_t *, window_t *);
+extern void disassociate_b(window_t *);
+extern void set_parse_state(buffer_t *, point_t);
+extern void set_parse_state2(buffer_t *, point_t);
+extern int parse_text(buffer_t *, point_t);
+extern void resize_terminal(void);
Index: usr.bin/atto/hilite.c
===================================================================
RCS file: usr.bin/atto/hilite.c
diff -N usr.bin/atto/hilite.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/hilite.c	16 Oct 2026 11:30:03 -0000
@@ -0,0 +1,108 @@
+/* hlite.c, generic syntax highlighting, Atto Emacs, Hugh Barney, Public Domain, 2016 */
+
+#include "header.h"
+
+int state = ID_DEFAULT;
+int next_state = ID_DEFAULT;
+int skip_count = 0;
+
+static char_t get_at(buffer_t *, point_t);
+static int is_symbol(char_t);
+
+static char_t get_at(buffer_t *bp, point_t pt)
+{
+	return (*ptr(bp, pt));
+}
+
+static char_t symbols[] = "{}[]()!'£$%^&*-+=:;@~#<>,.?/\\|";
+
+static int is_symbol(char_t c)
+{
+	char_t *p = symbols;
+
+	for (p = symbols; *p != '\0'; p++)
+		if (*p == c) return 1;
+	return 0;
+}
+
+void set_parse_state(buffer_t *bp, point_t pt)
+{
+	point_t po;
+
+	state = ID_DEFAULT;
+	next_state = ID_DEFAULT;
+	skip_count = 0;
+
+	for (po =0; po < pt; po++)
+		parse_text(bp, po);
+}
+
+int parse_text(buffer_t *bp, point_t pt)
+{
+	if (skip_count-- > 0)
+		return state;
+
+	char_t c_now = get_at(bp, pt);
+	char_t c_next = get_at(bp, pt + 1);
+	state = next_state;
+
+	if (state == ID_DEFAULT && c_now == '/' && c_next == '*') {
+		skip_count = 1;
+		return (next_state = state = ID_BLOCK_COMMENT);
+	}
+
+	if (state == ID_BLOCK_COMMENT && c_now == '*' && c_next == '/') {
+		skip_count = 1;
+		next_state = ID_DEFAULT;
+		return ID_BLOCK_COMMENT;
+	}
+
+	if (state == ID_DEFAULT && c_now == '/' && c_next == '/') {
+		skip_count = 1;
+		return (next_state = state = ID_LINE_COMMENT);
+	}
+
+	if (state == ID_LINE_COMMENT && c_now == '\n')
+		return (next_state = ID_DEFAULT);
+
+	if (state == ID_DEFAULT && c_now == '"')
+		return (next_state = ID_DOUBLE_STRING);
+
+	if (state == ID_DOUBLE_STRING && c_now == '\\') {
+		skip_count = 1;
+		return (next_state = ID_DOUBLE_STRING);
+	}
+
+	if (state == ID_DOUBLE_STRING && c_now == '"') {
+		next_state = ID_DEFAULT;
+		return ID_DOUBLE_STRING;
+	}
+
+	if (state == ID_DEFAULT && c_now == '\'')
+		return (next_state = ID_SINGLE_STRING);
+
+	if (state == ID_SINGLE_STRING && c_now == '\\') {
+		skip_count = 1;
+		return (next_state = ID_SINGLE_STRING);
+	}
+
+	if (state == ID_SINGLE_STRING && c_now == '\'') {
+		next_state = ID_DEFAULT;
+		return ID_SINGLE_STRING;
+	}
+
+	if (state != ID_DEFAULT)
+		return (next_state = state);
+
+	if (state == ID_DEFAULT && c_now >= '0' && c_now <= '9') {
+		next_state = ID_DEFAULT;
+		return (state = ID_DIGITS);
+	}
+
+	if (state == ID_DEFAULT && 1 == is_symbol(c_now)) {
+		next_state = ID_DEFAULT;
+		return (state = ID_SYMBOL);
+	}
+
+	return (next_state = state);
+}
Index: usr.bin/atto/key.c
===================================================================
RCS file: usr.bin/atto/key.c
diff -N usr.bin/atto/key.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/key.c	16 Oct 2026 11:30:03 -0000
@@ -0,0 +1,167 @@
+/* key.c, Atto Emacs, Public Domain, Hugh Barney, 2016, Derived from: Anthony's Editor January 93 */
+
+#include "header.h"
+
+/* desc, keys, func */
+keymap_t keymap[] = {
+	{"C-a beginning-of-line    ", "\x01", lnbegin },
+	{"C-b backward-char        ", "\x02", left },
+	{"C-d delete               ", "\x04", delete },
+	{"C-e end-of-line          ", "\x05", lnend },
+	{"C-f foward-char          ", "\x06", right },
+	{"C-h backspace            ", "\x08", backsp },
+	{"C-k kill-to-eol          ", "\x0B", killtoeol },
+	{"C-l refresh              ", "\x0C", redraw },
+	{"C-n next-line            ", "\x0E", down },
+	{"C-p previous-line        ", "\x10", up },
+	{"C-r search               ", "\x12", search },
+	{"C-s search               ", "\x13", search },
+	{"C-v forward-page         ", "\x16", pgdown },
+	{"C-w kill-region          ", "\x17", cut},
+	{"C-y yank                 ", "\x19", paste},
+	{"C-space set-mark         ", "\x00", iblock },  /* ctrl-space */
+	{"C-x 1 delete-other-window", "\x18\x31", delete_other_windows },
+	{"C-x 2 split-window       ", "\x18\x32", split_window },
+	{"C-x o other-window       ", "\x18\x6F", next_window },
+	{"C-x = cursor-position    ", "\x18\x3D", showpos },
+	{"C-x i insert-file        ", "\x18\x69", insertfile },
+	{"C-x k kill-buffer        ", "\x18\x6B", killbuffer },
+	{"C-x C-n next-buffer      ", "\x18\x0E", next_buffer },
+	{"C-x n next-buffer        ", "\x18\x6E", next_buffer },
+	{"C-x C-f find-file        ", "\x18\x06", readfile },
+	{"C-x C-s save-buffer      ", "\x18\x13", savebuffer },  
+	{"C-x C-w write-file       ", "\x18\x17", writefile },  /* write and prompt for name */
+	{"C-x C-c exit             ", "\x18\x03", quit_ask },
+	{"esc b back-word          ", "\x1B\x62", wleft },
+	{"esc f forward-word       ", "\x1B\x66", wright },
+	{"esc g gotoline           ", "\x1B\x67", gotoline },
+	{"esc k kill-region        ", "\x1B\x6B", cut },
+	{"esc r query-replace      ", "\x1B\x72", query_replace },
+	{"esc v backward-page      ", "\x1B\x76", pgup },
+	{"esc w copy-region        ", "\x1B\x77", copy},
+	{"esc @ set-mark           ", "\x1B\x40", iblock },  /* esc-@ */
+	{"esc < beg-of-buf         ", "\x1B\x3C", top },
+	{"esc > end-of-buf         ", "\x1B\x3E", bottom },
+	{"esc home, beg-of-buf     ", "\x1B\x1B\x4F\x48", top },
+	{"esc end, end-of-buf      ", "\x1B\x1B\x4F\x46", bottom },
+	{"esc up, beg-of-buf       ", "\x1B\x1B\x5B\x41", top },
+	{"esc down, end-of-buf     ", "\x1B\x1B\x5B\x42", bottom },
+	{"esc esc show-version     ", "\x1B\x1B", version },
+	{"ins toggle-overwrite-mode", "\x1B\x5B\x32\x7E", toggle_overwrite_mode }, /* Ins key */
+	{"del forward-delete-char  ", "\x1B\x5B\x33\x7E", delete }, /* Del key */
+	{"backspace delete-left    ", "\x7f", backsp },
+	{"up previous-line         ", "\x1B\x5B\x41", up },
+	{"down next-line           ", "\x1B\x5B\x42", down },
+	{"left backward-character  ", "\x1B\x5B\x44", left },
+	{"right forward-character  ", "\x1B\x5B\x43", right },
+	{"home beginning-of-line   ", "\x1B\x4F\x48", lnbegin },
+	{"end end-of-line          ", "\x1B\x4F\x46", lnend },
+	{"home beginning-of-line   ", "\x1B\x5B\x48", lnbegin },
+	{"end end-of-line          ", "\x1B\x5B\x46", lnend },
+	{"pgup backward-page       ", "\x1B\x5B\x35\x7E",pgup }, /* PgUp key */
+	{"pgdn forward-page        ", "\x1B\x5B\x36\x7E", pgdown }, /* PgDn key */
+        {"resize resize-terminal   ", "\x9A",     resize_terminal },
+	{"K_ERROR                  ", NULL, NULL }
+};
+
+char_t *get_key(keymap_t *keys, keymap_t **key_out)
+{
+	keymap_t *k;
+	int submatch;
+	static char_t buffer[K_BUFFER_LENGTH];
+	static char_t *record = buffer;
+
+	*key_out = NULL;
+
+	/* if recorded bytes remain, return next recorded byte. */
+	if (*record != '\0') {
+		*key_out = NULL;
+		return record++;
+	}
+	/* reset record buffer. */
+	record = buffer;
+
+	do {
+		assert(K_BUFFER_LENGTH > record - buffer);
+		/* read and record one byte. */
+		*record++ = (unsigned)getch();
+		*record = '\0';
+
+		/* if recorded bytes match any multi-byte sequence... */
+		for (k = keys, submatch = 0; k->key_bytes != NULL; ++k) {
+			const char_t *p, *q;
+
+			for (p = buffer, q = (const char_t *)k->key_bytes; *p == *q; ++p, ++q) {
+			        /* an exact match */
+				if (*q == '\0' && *p == '\0') {
+	    				record = buffer;
+					*record = '\0';
+					*key_out = k;
+					return record; /* empty string */
+				}
+			}
+			/* record bytes match part of a command sequence */
+			if (*p == '\0' && *q != '\0') {
+				submatch = 1;
+			}
+		}
+	} while (submatch);
+	/* nothing matched, return recorded bytes. */
+	record = buffer;
+	return (record++);
+}
+
+int getinput(const char *prompt, char *buf, int nbuf, int flag)
+{
+	int cpos = 0;
+	int c;
+	int start_col = strlen(prompt);
+
+	mvaddstr(MSGLINE, 0, prompt);
+	clrtoeol();
+
+	if (flag == F_CLEAR) buf[0] = '\0';
+
+	/* if we have a default value print it and go to end of it */
+	if (buf[0] != '\0') {
+		addstr(buf);
+		cpos = strlen(buf);
+	}
+
+	for (;;) {
+		refresh();
+		c = getch();
+		/* ignore control keys other than backspace, cr, lf */
+		if (c < 32 && c != 0x07 && c != 0x08 && c != 0x0a && c != 0x0d)
+			continue;
+
+		switch(c) {
+		case 0x0a: /* cr, lf */
+		case 0x0d:
+			buf[cpos] = '\0';
+			return (cpos > 0 ? TRUE : FALSE);
+
+		case 0x07: /* ctrl-g */
+			return FALSE;
+
+		case 0x7f: /* del, erase */
+		case 0x08: /* backspace */
+			if (cpos == 0)
+				continue;
+
+			move(MSGLINE, start_col + cpos - 1);
+			addch(' ');
+			move(MSGLINE, start_col + cpos - 1);
+			buf[--cpos] = '\0';
+			break;
+
+		default:
+			if (cpos < nbuf -1) {
+				addch(c);
+				buf[cpos++] = c;
+				buf[cpos] ='\0';
+			}
+			break;
+		}
+	}
+}
Index: usr.bin/atto/main.c
===================================================================
RCS file: usr.bin/atto/main.c
diff -N usr.bin/atto/main.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/main.c	16 Oct 2026 11:30:03 -0000
@@ -0,0 +1,134 @@
+/* main.c, Atto Emacs, Public Domain, Hugh Barney, 2016, Derived from: Anthony's Editor January 93 */
+
+#include "header.h"
+
+int done;
+int use_color;
+point_t nscrap;
+char_t *scrap;
+
+char_t *input;
+int msgflag;
+char msgline[TEMPBUF];
+char temp[TEMPBUF];
+char searchtext[STRBUF_M];
+char replace[STRBUF_M];
+
+keymap_t *key_return;
+keymap_t *key_map;
+buffer_t *curbp;			/* current buffer */
+buffer_t *bheadp;			/* head of list of buffers */
+window_t *curwp;
+window_t *wheadp;
+
+static void
+usage(void)
+{
+	(void)fprintf(stderr, "usage:\tatto [-C] [file ...]\n");
+	exit(EXIT_FAILURE);
+}
+
+int main(int argc, char **argv)
+{
+	extern char *optarg;
+	extern int optind;
+	int ch;
+	int i;
+
+	while ((ch = getopt(argc, argv, "C")) != -1) {
+		switch (ch) {
+		case 'C':
+			use_color = 1;
+			break;
+		default:
+			usage();
+			break;
+		}
+	}
+
+	setlocale(LC_ALL, "") ; /* required for 3,4 byte UTF8 chars */
+	if (initscr() == NULL) fatal("%s: Failed to initialize the screen.\n");
+	raw();
+	noecho();
+	idlok(stdscr, TRUE);
+
+	argc -= optind;
+	argv += optind;
+
+	if (use_color) {
+		start_color();
+		init_pair(ID_DEFAULT, COLOR_WHITE, COLOR_BLACK);         /* alpha */
+		init_pair(ID_SYMBOL, COLOR_CYAN, COLOR_BLACK);           /* non alpha, non digit */
+		init_pair(ID_MODELINE, COLOR_BLACK, COLOR_WHITE);        /* modeline */
+		init_pair(ID_DIGITS, COLOR_YELLOW, COLOR_BLACK);         /* digits */
+		init_pair(ID_BLOCK_COMMENT, COLOR_GREEN, COLOR_BLACK);   /* block comments */
+		init_pair(ID_LINE_COMMENT, COLOR_GREEN, COLOR_BLACK);    /* line comments */
+		init_pair(ID_SINGLE_STRING, COLOR_YELLOW, COLOR_BLACK);  /* single quoted strings */
+		init_pair(ID_DOUBLE_STRING, COLOR_YELLOW, COLOR_BLACK);  /* double quoted strings */
+	}
+
+	if (argc) {
+		for (i = 0; i < argc; ++i) {
+			curbp = find_buffer(argv[i], TRUE);
+			(void) insert_file(argv[i], FALSE);
+			/* Save filename regardless of load() success. */
+			snprintf(curbp->b_fname, NAME_MAX, "%s", argv[i]);
+		}
+	} else {
+		curbp = find_buffer("*scratch*", TRUE);
+		snprintf(curbp->b_bname, NAME_MAX, "*scratch*");
+	}
+
+	wheadp = curwp = new_window();
+	one_window(curwp);
+	associate_b2w(curbp, curwp);
+
+	if (!growgap(curbp, CHUNK)) fatal("%s: Failed to allocate required memory.\n");
+	key_map = keymap;
+
+	while (!done) {
+		update_display();
+		input = get_key(key_map, &key_return);
+
+		if (key_return != NULL) {
+			(key_return->func)();
+		} else {
+			/* allow TAB and NEWLINE, otherwise any Control Char is 'Not bound' */
+			if (*input > 31 || *input == 10 || *input == 9)
+				insert();
+                        else {
+				flushinp(); /* discard without writing in buffer */
+				msg("Not bound");
+			}
+		}
+	}
+
+	if (scrap != NULL) free(scrap);
+	move(LINES-1, 0);
+	refresh();
+	noraw();
+	endwin();
+	return 0;
+}
+
+void fatal(const char *msg)
+{
+	if (curscr != NULL) {
+		move(LINES-1, 0);
+		refresh();
+		noraw();
+		endwin();
+		putchar('\n');
+	}
+	fprintf(stderr, msg, PROG_NAME);
+	exit(1);
+}
+
+void msg(const char *msg, ...)
+{
+	va_list args;
+	va_start(args, msg);
+	(void)vsprintf(msgline, msg, args);
+	va_end(args);
+	msgflag = TRUE;
+}
Index: usr.bin/atto/replace.c
===================================================================
RCS file: usr.bin/atto/replace.c
diff -N usr.bin/atto/replace.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/replace.c	16 Oct 2026 11:30:03 -0000
@@ -0,0 +1,103 @@
+/* replace.c, Atto Emacs, Hugh Barney, Public Domain, 2016 */
+
+#include <string.h>
+#include "header.h"
+
+/*search for a string and replace it with another string */
+void query_replace(void)
+{
+	point_t o_point = curbp->b_point;
+	point_t l_point = -1;
+	point_t found;
+	char question[STRBUF_L];
+	int slen, rlen;   /* length of search and replace strings */
+	int numsub = 0;   /* number of substitutions */
+	int ask = TRUE;
+	int c;
+
+	searchtext[0] = '\0';
+	replace[0] = '\0';
+	
+	if (!getinput("Query replace: ", (char*)searchtext, STRBUF_M, F_CLEAR))
+		return;
+
+	if (!getinput("With: ", (char*)replace, STRBUF_M, F_CLEAR))
+		return;
+
+	slen = strlen(searchtext);
+	rlen = strlen(replace);
+	
+	/* build query replace question string */
+	snprintf(question, sizeof(question), "Replace '%s' with '%s' ? ", searchtext, replace);
+
+	/* scan through the file, from point */
+	numsub = 0;
+	while(TRUE) {
+		found = search_forward(curbp, curbp->b_point, searchtext);
+
+		/* if not found set the point to the last point of replacement, or where we started */
+		if (found == -1) {
+			curbp->b_point = (l_point == -1 ? o_point : l_point);
+			break;
+		}
+
+		curbp->b_point = found;
+		/* search_forward places point at end of search, move to start of search */
+		curbp->b_point -= slen;
+
+		if (ask == TRUE) {
+			msg(question);
+			clrtoeol();
+			
+		qprompt:
+			display(curwp, TRUE);
+			c = getch();
+
+			switch (c) {
+			case 'y': /* yes, substitute */
+				break;
+			
+			case 'n': /* no, find next */
+				curbp->b_point = found; /* set to end of search string */
+				continue;
+			
+			case '!': /* yes/stop asking, do the lot */
+				ask = FALSE;
+				break;
+			
+			case 0x1B: /* esc */
+				flushinp(); /* discard any escape sequence without writing in buffer */
+			case 'q': /* controlled exit */
+				return;
+
+			default: /* help me */
+				msg("(y)es, (n)o, (!)do the rest, (q)uit");
+				goto qprompt;
+			}
+		}
+		
+		if (rlen > slen) {
+			movegap(curbp, found);
+			/*check enough space in gap left */
+			if (rlen - slen < curbp->b_egap - curbp->b_gap)
+				growgap(curbp, rlen - slen);
+			/* shrink gap right by r - s */
+			curbp->b_gap = curbp->b_gap + (rlen - slen);
+		} else if (slen > rlen) {
+			movegap(curbp, found);
+			/* stretch gap left by s - r, no need to worry about space */
+			curbp->b_gap = curbp->b_gap - (slen - rlen);
+		} else {
+			/* if rlen = slen, we just overwrite the chars, no need to move gap */		
+		}
+
+		/* now just overwrite the chars at point in the buffer */
+		l_point = curbp->b_point;
+		memcpy(ptr(curbp, curbp->b_point), replace, rlen * sizeof (char_t));
+		curbp->b_flags |= B_MODIFIED;
+		curbp->b_point = found - (slen - rlen); /* end of replcement */
+		numsub++;
+	}
+
+	msg("%d substitutions", numsub);
+}
Index: usr.bin/atto/search.c
===================================================================
RCS file: usr.bin/atto/search.c
diff -N usr.bin/atto/search.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/search.c	16 Oct 2026 11:30:03 -0000
@@ -0,0 +1,116 @@
+/* search.c, Atto Emacs, Public Domain, Hugh Barney, 2016, Derived from: Anthony's Editor January 93 */
+
+#include "header.h"
+
+#define FWD_SEARCH 1
+#define REV_SEARCH 2
+
+void search(void)
+{
+	int cpos = 0;	
+	int c;
+	point_t o_point = curbp->b_point;
+	point_t found;
+
+	searchtext[0] = '\0';
+	display_prompt_and_response("Search: ", searchtext);
+	cpos = strlen(searchtext);
+
+	for (;;) {
+		c = getch();
+		/* ignore control keys other than C-g, backspace, CR,  C-s, C-R, ESC */
+		if (c < 32 && c != 07 && c != 0x08 && c != 0x13 && c != 0x12 && c != 0x1b)
+			continue;
+
+		switch(c) {
+		case 0x1b: /* esc */
+			searchtext[cpos] = '\0';
+			flushinp(); /* discard any escape sequence without writing in buffer */
+			return;
+
+		case 0x07: /* ctrl-g */
+			curbp->b_point = o_point;
+			return;
+
+		case 0x13: /* ctrl-s, do the search */
+			found = search_forward(curbp, curbp->b_point, searchtext);
+			display_search_result(found, FWD_SEARCH, "Search: ", searchtext);
+			break;
+
+		case 0x12: /* ctrl-r, do the search */
+			found = search_backwards(curbp, curbp->b_point, searchtext);
+			display_search_result(found, REV_SEARCH, "Search: ", searchtext);
+			break;
+			
+		case 0x7f: /* del, erase */
+		case 0x08: /* backspace */
+			if (cpos == 0)
+				continue;
+			searchtext[--cpos] = '\0';
+			display_prompt_and_response("Search: ", searchtext);
+			break;
+
+		default:	
+			if (cpos < STRBUF_M - 1) {
+				searchtext[cpos++] = c;
+				searchtext[cpos] = '\0';
+				display_prompt_and_response("Search: ", searchtext);
+			}
+			break;
+		}
+	}
+}
+
+void display_search_result(point_t found, int dir, const char *prompt, const char *search)
+{
+	if (found != -1 ) {
+		curbp->b_point = found;
+		msg("%s%s",prompt, search);
+		display(curwp, TRUE);
+	} else {
+		msg("Failing %s%s",prompt, search);
+		dispmsg();
+		curbp->b_point = (dir == FWD_SEARCH ? 0 : pos(curbp, curbp->b_ebuf));
+	}
+}
+
+point_t search_forward(buffer_t *bp, point_t start_p, char *stext)
+{
+	point_t end_p = pos(bp, bp->b_ebuf);
+	point_t p,pp;
+	char* s;
+
+	if (0 == strlen(stext))
+		return start_p;
+
+	for (p=start_p; p < end_p; p++) {
+		for (s=stext, pp=p; *s == *(ptr(bp, pp)) && *s !='\0' && pp < end_p; s++, pp++)
+			;
+
+		if (*s == '\0')
+			return pp;
+	}
+
+	return -1;
+}
+
+point_t search_backwards(buffer_t *bp, point_t start_p, char *stext)
+{
+	point_t p,pp;
+	char* s;
+	
+	if (0 == strlen(stext))
+		return start_p;
+
+	for (p=start_p; p >= 0; p--) {
+		for (s=stext, pp=p; *s == *(ptr(bp, pp)) && *s != '\0' && pp > -1; s++, pp++)
+			;
+		
+		if (*s == '\0') {
+			if (p > 0)
+				p--;
+			return p;
+		}
+	}
+	return -1;
+}
Index: usr.bin/atto/window.c
===================================================================
RCS file: usr.bin/atto/window.c
diff -N usr.bin/atto/window.c
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ usr.bin/atto/window.c	16 Oct 2026 11:30:03 -0000
@@ -0,0 +1,104 @@
+/* window.c, Atto Emacs, Hugh Barney, Public Domain, 2015 */
+
+#include "header.h"
+
+int win_cnt = 0;
+
+window_t* new_window(void)
+{
+	window_t *wp = (window_t *)malloc(sizeof(window_t));
+	
+	assert(wp != NULL); /* call fatal instead XXX */
+	wp->w_next = NULL;
+	wp->w_bufp = NULL;
+	wp->w_point = 0;
+	wp->w_mark = NOMARK;
+	wp->w_top = 0;	
+	wp->w_rows = 0;	
+	wp->w_update = FALSE;
+	sprintf(wp->w_name, "W%d", ++win_cnt);
+	return wp;
+}
+
+void one_window(window_t *wp)
+{
+	wp->w_top = 0;
+	wp->w_rows = LINES - 2;
+	wp->w_next = NULL;
+}
+
+void split_window(void)
+{
+	window_t *wp, *wp2;
+	int ntru, ntrl;
+
+	if (curwp->w_rows < 3) {
+		msg("Cannot split a %d line window", curwp->w_rows);
+		return;
+	}
+	
+	wp = new_window();	
+	associate_b2w(curwp->w_bufp,wp);
+	b2w(wp); /* inherit buffer settings */
+  
+	ntru = (curwp->w_rows - 1) / 2; /* Upper size */
+	ntrl = (curwp->w_rows - 1) - ntru; /* Lower size */
+
+	/* Old is upper window */
+	curwp->w_rows = ntru;
+	wp->w_top = curwp->w_top + ntru + 1;
+	wp->w_rows = ntrl;
+
+	/* insert it in the list */
+	wp2 = curwp->w_next;
+	curwp->w_next = wp;
+	wp->w_next = wp2;
+	redraw(); /* mark the lot for update */
+}
+
+void next_window(void) {
+	curwp->w_update = TRUE; /* make sure modeline gets updated */
+	curwp = (curwp->w_next == NULL ? wheadp : curwp->w_next);
+	curbp = curwp->w_bufp;
+	
+	if (curbp->b_cnt > 1)
+		w2b(curwp); /* push win vars to buffer */
+}
+
+void delete_other_windows(void)
+{
+	if (wheadp->w_next == NULL) {
+		msg("Only 1 window");
+		return;
+	}
+	free_other_windows(curwp);
+}
+	
+void free_other_windows(window_t *winp)
+{
+	window_t *wp, *next;
+
+	for (wp = next = wheadp; next != NULL; wp = next) {
+		next = wp->w_next; /* get next before a call to free() makes wp undefined */
+		if (wp != winp) {
+			disassociate_b(wp); /* this window no longer references its buffer */
+			free(wp);
+		}
+	}
+	
+	wheadp = curwp = winp;
+	one_window(winp);
+}
+
+void associate_b2w(buffer_t *bp, window_t *wp) {
+	assert(bp != NULL);
+	assert(wp != NULL);
+	wp->w_bufp = bp;
+	bp->b_cnt++;
+}
+
+void disassociate_b(window_t *wp) {
+	assert(wp != NULL);
+	assert(wp->w_bufp != NULL);
+	wp->w_bufp->b_cnt--;
+}

Reply via email to