turbovi 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- turbovi-0.3.0/PKG-INFO +78 -0
- turbovi-0.3.0/README.md +56 -0
- turbovi-0.3.0/pyproject.toml +80 -0
- turbovi-0.3.0/src/turbovi/__init__.py +16 -0
- turbovi-0.3.0/src/turbovi/__main__.py +198 -0
- turbovi-0.3.0/src/turbovi/edits.py +262 -0
- turbovi-0.3.0/src/turbovi/highlight.py +140 -0
- turbovi-0.3.0/src/turbovi/keymap.py +1067 -0
- turbovi-0.3.0/src/turbovi/persist.py +135 -0
- turbovi-0.3.0/src/turbovi/screen.py +258 -0
- turbovi-0.3.0/src/turbovi/session.py +664 -0
- turbovi-0.3.0/src/turbovi/state.py +356 -0
- turbovi-0.3.0/src/turbovi/substitute.py +248 -0
- turbovi-0.3.0/src/turbovi/textfile.py +119 -0
turbovi-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: turbovi
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A vi clone for the terminal: enough vim to edit with, in three thousand lines
|
|
5
|
+
Keywords: terminal,tui,vi,vim,editor,modal
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: POSIX
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Text Editors
|
|
17
|
+
Classifier: Topic :: Terminals
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Dist: turbodesk[syntax]>=0.2
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# TurboVI
|
|
24
|
+
|
|
25
|
+
A vi clone for the terminal: modes, counts, operators, registers, marks, macros, split windows and `:s`, in about three thousand lines of Python.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
uv sync
|
|
29
|
+
uv run turbovi somefile.py
|
|
30
|
+
uv run turbovi one.py two.py # two buffers, :bn between them
|
|
31
|
+
uv run turbovi # a scratch buffer
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`:q` to leave, `:help` for what is here.
|
|
35
|
+
|
|
36
|
+
## What a vim user gets
|
|
37
|
+
|
|
38
|
+
**The grammar**, `["x] [count] operator [count] motion`, over `hjkl w b e W B E 0 ^ $ gg G { } f F t T %`. Operators `d c y > <` with doubling (`dd`, `yy`, `cc`), text objects (`iw aw i( a" i{`), and `x X D C s S r ~ J p P o O`.
|
|
39
|
+
|
|
40
|
+
**Modes**: normal, insert, visual, visual-line and command.
|
|
41
|
+
|
|
42
|
+
**Paging** with `<C-f>` `<C-b>` `<C-d>` `<C-u>`, by the window rather than by a fixed number, and `<C-e>` / `<C-y>` to move the view without moving the cursor.
|
|
43
|
+
|
|
44
|
+
**Search** with `/ ? n N` and match highlighting. **Undo and redo** with `u` and `<C-r>`. **`.`** repeats the last change, recorded as keystrokes the way vi does it, so a change that ends in insert mode replays whole.
|
|
45
|
+
|
|
46
|
+
**`:s`** with ranges (`%`, `.`, `$`, `1,5`, `.+1`), the `g`, `i` and `n` flags, any non-alphanumeric delimiter, and `&` in the replacement. One `u` undoes the whole substitution.
|
|
47
|
+
|
|
48
|
+
**Marks**: `ma` puts one down, `` `a `` comes back to the character and `'a` to the first non-blank of that line. Every jump leaves a mark, so `` `` `` returns from a `G` or a search. `:marks` lists them. They are not shifted when the text moves, which is the one place this is smaller than vi.
|
|
49
|
+
|
|
50
|
+
**Macros**: `qa` records into register `a`, `q` stops, `@a` plays, `3@a` plays three times, `@@` repeats the last. They share storage with the yank registers the way vi's do, so `"ap` pastes a macro as text and `@a` runs a yanked line. A macro that calls itself stops with a message rather than a stack that runs out.
|
|
51
|
+
|
|
52
|
+
**Several files at once** with `:e :ls :bn :bp :b`, split windows with `:sp` and `:vsp`, and `<C-w>` then `w s v c o` or `h j k l` to move between them. `<C-p>` and `:find` are a fuzzy picker over the project: type a few letters of the path and it opens in the current window.
|
|
53
|
+
|
|
54
|
+
**The system clipboard** on `"+y` and `"+p`, over OSC 52 and a local helper, so it crosses ssh.
|
|
55
|
+
|
|
56
|
+
**Syntax colouring** from the file's name. `:set syntax=rust` colours a file with no suffix, `:set nosyntax` stops. A search hit and a selection paint over the colouring, because finding something inside a comment has to be visible.
|
|
57
|
+
|
|
58
|
+
**A file opens and saves as itself.** The encoding, the line endings and whether the last line had a newline are read once and written back unchanged. A latin-1 file stays latin-1, a CRLF file stays CRLF, and a file with no trailing newline does not gain one.
|
|
59
|
+
|
|
60
|
+
## Settings and recovery
|
|
61
|
+
|
|
62
|
+
Settings live in `$XDG_CONFIG_HOME/turbovi/config`, a file of ex commands the way a `vimrc` is:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
set nu
|
|
66
|
+
set rnu
|
|
67
|
+
sp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A modified buffer is copied to `$XDG_DATA_HOME/turbovi/recover/` every few seconds, keyed by a hash of its absolute path, and `:recover` puts it back after a crash without writing anything to disk. The recovery copy is dropped when the buffer is saved.
|
|
71
|
+
|
|
72
|
+
## What it is not
|
|
73
|
+
|
|
74
|
+
Not a vim: no plugins, no scripting language, no `:g`, no folds, no tabs, no persistent undo across sessions yet.
|
|
75
|
+
|
|
76
|
+
## Built on turbodesk
|
|
77
|
+
|
|
78
|
+
[turbodesk](https://turbodesk.lab.abilian.com) is the immediate-mode terminal UI library underneath. This editor is the application on it that uses **no widgets at all** — views, events and hooks, nothing else — which is what proved the core carries a real application on its own. It lived in that repository as `examples/vi/` until 2026-08-26; a smaller showcase version stays there.
|
turbovi-0.3.0/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# TurboVI
|
|
2
|
+
|
|
3
|
+
A vi clone for the terminal: modes, counts, operators, registers, marks, macros, split windows and `:s`, in about three thousand lines of Python.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
uv sync
|
|
7
|
+
uv run turbovi somefile.py
|
|
8
|
+
uv run turbovi one.py two.py # two buffers, :bn between them
|
|
9
|
+
uv run turbovi # a scratch buffer
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`:q` to leave, `:help` for what is here.
|
|
13
|
+
|
|
14
|
+
## What a vim user gets
|
|
15
|
+
|
|
16
|
+
**The grammar**, `["x] [count] operator [count] motion`, over `hjkl w b e W B E 0 ^ $ gg G { } f F t T %`. Operators `d c y > <` with doubling (`dd`, `yy`, `cc`), text objects (`iw aw i( a" i{`), and `x X D C s S r ~ J p P o O`.
|
|
17
|
+
|
|
18
|
+
**Modes**: normal, insert, visual, visual-line and command.
|
|
19
|
+
|
|
20
|
+
**Paging** with `<C-f>` `<C-b>` `<C-d>` `<C-u>`, by the window rather than by a fixed number, and `<C-e>` / `<C-y>` to move the view without moving the cursor.
|
|
21
|
+
|
|
22
|
+
**Search** with `/ ? n N` and match highlighting. **Undo and redo** with `u` and `<C-r>`. **`.`** repeats the last change, recorded as keystrokes the way vi does it, so a change that ends in insert mode replays whole.
|
|
23
|
+
|
|
24
|
+
**`:s`** with ranges (`%`, `.`, `$`, `1,5`, `.+1`), the `g`, `i` and `n` flags, any non-alphanumeric delimiter, and `&` in the replacement. One `u` undoes the whole substitution.
|
|
25
|
+
|
|
26
|
+
**Marks**: `ma` puts one down, `` `a `` comes back to the character and `'a` to the first non-blank of that line. Every jump leaves a mark, so `` `` `` returns from a `G` or a search. `:marks` lists them. They are not shifted when the text moves, which is the one place this is smaller than vi.
|
|
27
|
+
|
|
28
|
+
**Macros**: `qa` records into register `a`, `q` stops, `@a` plays, `3@a` plays three times, `@@` repeats the last. They share storage with the yank registers the way vi's do, so `"ap` pastes a macro as text and `@a` runs a yanked line. A macro that calls itself stops with a message rather than a stack that runs out.
|
|
29
|
+
|
|
30
|
+
**Several files at once** with `:e :ls :bn :bp :b`, split windows with `:sp` and `:vsp`, and `<C-w>` then `w s v c o` or `h j k l` to move between them. `<C-p>` and `:find` are a fuzzy picker over the project: type a few letters of the path and it opens in the current window.
|
|
31
|
+
|
|
32
|
+
**The system clipboard** on `"+y` and `"+p`, over OSC 52 and a local helper, so it crosses ssh.
|
|
33
|
+
|
|
34
|
+
**Syntax colouring** from the file's name. `:set syntax=rust` colours a file with no suffix, `:set nosyntax` stops. A search hit and a selection paint over the colouring, because finding something inside a comment has to be visible.
|
|
35
|
+
|
|
36
|
+
**A file opens and saves as itself.** The encoding, the line endings and whether the last line had a newline are read once and written back unchanged. A latin-1 file stays latin-1, a CRLF file stays CRLF, and a file with no trailing newline does not gain one.
|
|
37
|
+
|
|
38
|
+
## Settings and recovery
|
|
39
|
+
|
|
40
|
+
Settings live in `$XDG_CONFIG_HOME/turbovi/config`, a file of ex commands the way a `vimrc` is:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
set nu
|
|
44
|
+
set rnu
|
|
45
|
+
sp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A modified buffer is copied to `$XDG_DATA_HOME/turbovi/recover/` every few seconds, keyed by a hash of its absolute path, and `:recover` puts it back after a crash without writing anything to disk. The recovery copy is dropped when the buffer is saved.
|
|
49
|
+
|
|
50
|
+
## What it is not
|
|
51
|
+
|
|
52
|
+
Not a vim: no plugins, no scripting language, no `:g`, no folds, no tabs, no persistent undo across sessions yet.
|
|
53
|
+
|
|
54
|
+
## Built on turbodesk
|
|
55
|
+
|
|
56
|
+
[turbodesk](https://turbodesk.lab.abilian.com) is the immediate-mode terminal UI library underneath. This editor is the application on it that uses **no widgets at all** — views, events and hooks, nothing else — which is what proved the core carries a real application on its own. It lived in that repository as `examples/vi/` until 2026-08-26; a smaller showcase version stays there.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "turbovi"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "A vi clone for the terminal: enough vim to edit with, in three thousand lines"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
keywords = ["terminal", "tui", "vi", "vim", "editor", "modal"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Operating System :: POSIX",
|
|
14
|
+
"Operating System :: POSIX :: Linux",
|
|
15
|
+
"Operating System :: MacOS :: MacOS X",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
"Topic :: Text Editors",
|
|
20
|
+
"Topic :: Terminals",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
# No `License ::` classifier: `license` above is an SPDX expression (PEP 639), and
|
|
24
|
+
# PyPI rejects a distribution that carries both.
|
|
25
|
+
|
|
26
|
+
# The syntax extra rather than plain turbodesk: `:set syntax` colours the buffer, and
|
|
27
|
+
# `turbodesk.syntax` is where Pygments lives.
|
|
28
|
+
dependencies = ["turbodesk[syntax]>=0.2"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
turbovi = "turbovi.__main__:main"
|
|
32
|
+
|
|
33
|
+
# [project.urls] once the repository is public. A project URL that 404s is worse than
|
|
34
|
+
# an absent one.
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["uv_build>=0.11.33,<0.12.0"]
|
|
38
|
+
build-backend = "uv_build"
|
|
39
|
+
|
|
40
|
+
[tool.uv.sources]
|
|
41
|
+
# turbodesk from the checkout beside this one, so a library edit is picked up live.
|
|
42
|
+
# Drop this and the version floor above takes over the day both are on PyPI.
|
|
43
|
+
turbodesk = { path = "../turbodesk", editable = true }
|
|
44
|
+
|
|
45
|
+
# Linting lives in ruff.toml.
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
pythonpath = ["src"]
|
|
50
|
+
addopts = "--strict-markers --import-mode=importlib"
|
|
51
|
+
markers = [
|
|
52
|
+
"unit: isolated and fast — no I/O, no event loop",
|
|
53
|
+
"integration: components wired together, may touch the filesystem",
|
|
54
|
+
"slow: needs an external program or wall-clock time",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.mypy]
|
|
58
|
+
# Read by zuban too. Same search path as pytest and the other checkers.
|
|
59
|
+
mypy_path = "src"
|
|
60
|
+
|
|
61
|
+
[tool.ty.environment]
|
|
62
|
+
extra-paths = ["src"]
|
|
63
|
+
|
|
64
|
+
[tool.pyrefly]
|
|
65
|
+
project-includes = ["**/*.py*"]
|
|
66
|
+
project-excludes = ["**/.venv/**"]
|
|
67
|
+
search-path = ["src", "tests"]
|
|
68
|
+
|
|
69
|
+
[dependency-groups]
|
|
70
|
+
dev = [
|
|
71
|
+
"mypy>=2.3.0",
|
|
72
|
+
"pre-commit>=4.6.1",
|
|
73
|
+
"pyrefly>=1.2.0",
|
|
74
|
+
"pytest>=9.1.1",
|
|
75
|
+
"pytest-cov>=7.1.0",
|
|
76
|
+
"ruff>=0.16.1",
|
|
77
|
+
"ty>=0.0.66",
|
|
78
|
+
"types-pygments>=2.20.0.20260728",
|
|
79
|
+
"zuban>=0.9.1",
|
|
80
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""TurboVI: a vi clone for the terminal, in about three thousand lines.
|
|
2
|
+
|
|
3
|
+
Modes, motions, operators, counts, registers, marks, macros, `.` repeat, `:s` with
|
|
4
|
+
ranges and flags, multiple buffers, split windows, syntax colouring, crash recovery,
|
|
5
|
+
and a config file that is a list of ex commands the way a `vimrc` is.
|
|
6
|
+
|
|
7
|
+
`state.py` is the value every keystroke transforms; `keymap.py` is the grammar over it,
|
|
8
|
+
pure functions with no `UI` import. `session.py` owns the buffers and windows, and
|
|
9
|
+
`screen.py` draws. Built on [turbodesk](https://turbodesk.lab.abilian.com), and the one
|
|
10
|
+
application on it that uses no widgets at all: the buffer is drawn from the view
|
|
11
|
+
combinators directly, which is what proved the core carries an application on its own.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""turbovi — a vi clone, enough vim to edit with, in under two thousand lines.
|
|
2
|
+
|
|
3
|
+
turbovi # scratch buffer
|
|
4
|
+
turbovi somefile.py
|
|
5
|
+
turbovi one.py two.py # two buffers, :bn between them
|
|
6
|
+
|
|
7
|
+
Modes, counts, operators, motions, text objects, registers, undo, search, `.` to repeat
|
|
8
|
+
the last change, `:s` to substitute, and a handful of other ex commands. Several files
|
|
9
|
+
at once with `:e` and `:bn`, split windows with `:sp` and `:vsp`, `<C-w>` to move
|
|
10
|
+
between them. Source is coloured by the file's name, and `:set syntax=rust` says so by
|
|
11
|
+
hand. `:q` to leave, `:help` for what is here.
|
|
12
|
+
|
|
13
|
+
Settings live in `$XDG_CONFIG_HOME/turbovi/config`, a file of ex commands the way a
|
|
14
|
+
`vimrc` is. A modified buffer is copied to `$XDG_DATA_HOME/turbovi/recover/` every few
|
|
15
|
+
seconds, and `:recover` puts it back after a crash.
|
|
16
|
+
|
|
17
|
+
Not a bonsai_term example. This one exists to find out whether the turbodesk core is
|
|
18
|
+
enough to build a real application on — no widgets, just views, events and hooks.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from dataclasses import replace
|
|
26
|
+
|
|
27
|
+
from turbodesk import UI, View, run
|
|
28
|
+
from turbodesk.events import Event, Key, KeyPress, Mod, Paste
|
|
29
|
+
from turbovi import persist, screen, session
|
|
30
|
+
from turbovi.session import Session
|
|
31
|
+
from turbovi.state import INSERT
|
|
32
|
+
|
|
33
|
+
HELP = (
|
|
34
|
+
"motions hjkl w b e W B E 0 ^ $ gg G { } f F t T % · "
|
|
35
|
+
"pages <C-f> <C-b> <C-d> <C-u> · scroll <C-e> <C-y> · counts 3j d2w · "
|
|
36
|
+
'operators d c y > < with dd yy cc · text objects iw aw i( a" · '
|
|
37
|
+
"x X D C s S r ~ J p P o O · v V visual · u <C-r> undo · "
|
|
38
|
+
"/ ? n N search · . repeat · qa..q @a @@ macros · ma `a 'a marks · "
|
|
39
|
+
'"+y "+p system clipboard · '
|
|
40
|
+
":w :q :wq :s :set nu/syntax :noh :recover · ZZ · "
|
|
41
|
+
":e :ls :bn :bp buffers · :sp :vsp :only splits · <C-w>wsvcjkhl windows"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
#: How often a modified buffer is copied somewhere it can be recovered from. Long
|
|
45
|
+
#: enough to cost nothing, short enough that a crash loses a sentence rather than a
|
|
46
|
+
#: paragraph.
|
|
47
|
+
RECOVERY_SECONDS = 5.0
|
|
48
|
+
|
|
49
|
+
SPECIAL_KEYS = {
|
|
50
|
+
Key.ESCAPE: "<Esc>",
|
|
51
|
+
Key.ENTER: "<CR>",
|
|
52
|
+
Key.BACKSPACE: "<BS>",
|
|
53
|
+
Key.TAB: "<Tab>",
|
|
54
|
+
Key.UP: "<Up>",
|
|
55
|
+
Key.DOWN: "<Down>",
|
|
56
|
+
Key.LEFT: "<Left>",
|
|
57
|
+
Key.RIGHT: "<Right>",
|
|
58
|
+
Key.DELETE: "<Del>",
|
|
59
|
+
Key.HOME: "<Home>",
|
|
60
|
+
Key.END: "<End>",
|
|
61
|
+
Key.PAGE_UP: "<C-b>",
|
|
62
|
+
Key.PAGE_DOWN: "<C-f>",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def encode(event: Event) -> str | None:
|
|
67
|
+
"""Input event to vim key notation, which is what the keymap parses."""
|
|
68
|
+
if isinstance(event, Paste):
|
|
69
|
+
return None
|
|
70
|
+
if not isinstance(event, KeyPress):
|
|
71
|
+
return None
|
|
72
|
+
if isinstance(event.key, str):
|
|
73
|
+
if Mod.CTRL in event.mods:
|
|
74
|
+
return f"<C-{event.key}>"
|
|
75
|
+
return event.key if Mod.META not in event.mods else None
|
|
76
|
+
return SPECIAL_KEYS.get(event.key)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def configured(live: Session) -> Session:
|
|
80
|
+
"""Apply the config file, which is a list of ex commands.
|
|
81
|
+
|
|
82
|
+
The loaded file's own message survives unless a config line complained, because the
|
|
83
|
+
thing worth reading on the first frame is usually "there is a recovery file" and a
|
|
84
|
+
config that ran cleanly has nothing to say.
|
|
85
|
+
"""
|
|
86
|
+
loaded = session.current(live).message
|
|
87
|
+
for line in persist.read_config():
|
|
88
|
+
live = session.command(
|
|
89
|
+
session.settled(live, session.current(live).noted("")), line
|
|
90
|
+
)
|
|
91
|
+
said = session.current(live).message
|
|
92
|
+
return live if said else session.settled(live, session.current(live).noted(loaded))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
#: Requests the app carries out rather than the session: both have to leave the render
|
|
96
|
+
#: loop, one for a subprocess and one for a modal.
|
|
97
|
+
SPAWNED = ("clip", "find")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def app(ui: UI, initial: Session) -> View:
|
|
101
|
+
stored, set_live = ui.state(initial)
|
|
102
|
+
|
|
103
|
+
# **Cleared before anything else reads it.** A spawned task does not run until this
|
|
104
|
+
# render returns, and a picker then waits on somebody typing at a modal, so a
|
|
105
|
+
# request left in place is re-read by every frame in between and spawned again each
|
|
106
|
+
# time. The first attempt cleared it *and then* let the scroll below write the old
|
|
107
|
+
# session back over the top — seventy-nine stacked modals, each revealed by closing
|
|
108
|
+
# the one on top of it. Everything downstream reads `live`, the cleared one.
|
|
109
|
+
pending = session.current(stored).request
|
|
110
|
+
spawning = bool(pending) and pending[0] in SPAWNED
|
|
111
|
+
live = (
|
|
112
|
+
session.settled(stored, replace(session.current(stored), request=()))
|
|
113
|
+
if spawning
|
|
114
|
+
else stored
|
|
115
|
+
)
|
|
116
|
+
editor = session.current(live)
|
|
117
|
+
if spawning:
|
|
118
|
+
if pending[0] == "clip":
|
|
119
|
+
ui.spawn(_clipboard(live, pending, set_live))
|
|
120
|
+
else:
|
|
121
|
+
ui.spawn(_find_file(live, ui, set_live))
|
|
122
|
+
|
|
123
|
+
# `live` here is one frame behind the scroll and the size settled below, and that
|
|
124
|
+
# is not a problem: setting an unequal value marks the frame dirty, so a session
|
|
125
|
+
# that has just measured a new terminal is re-rendered before any key is delivered
|
|
126
|
+
# and the handler that sees the key closes over the settled value.
|
|
127
|
+
@ui.on_event
|
|
128
|
+
def on_key(event: Event) -> bool:
|
|
129
|
+
key = encode(event)
|
|
130
|
+
if key is None:
|
|
131
|
+
return False
|
|
132
|
+
if key == "<C-c>": # vi answers Ctrl-C with a nudge, it does not quit
|
|
133
|
+
set_live(session.settled(live, editor.noted("type :q to exit")))
|
|
134
|
+
return True
|
|
135
|
+
set_live(session.apply(live, key))
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
# Keep a copy of every buffer that differs from its file, and drop it the moment it
|
|
139
|
+
# does not. `:w` clears `modified`, so writing takes the copy away by itself.
|
|
140
|
+
ui.every(RECOVERY_SECONDS, lambda: _keep(live))
|
|
141
|
+
|
|
142
|
+
if editor.quit:
|
|
143
|
+
for buffer in live.buffers:
|
|
144
|
+
if buffer.path:
|
|
145
|
+
persist.drop_recovery(buffer.path)
|
|
146
|
+
ui.exit()
|
|
147
|
+
|
|
148
|
+
# Setting an equal value does not schedule a frame, so storing the scroll this frame
|
|
149
|
+
# settled on is free on every frame that did not scroll.
|
|
150
|
+
scrolled = session.followed(live, ui.size)
|
|
151
|
+
set_live(scrolled)
|
|
152
|
+
|
|
153
|
+
ui.set_cursor(
|
|
154
|
+
screen.CURSOR, kind="bar-blinking" if editor.mode == INSERT else "block"
|
|
155
|
+
)
|
|
156
|
+
return session.render(scrolled, ui.size, ui.theme)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def _find_file(
|
|
160
|
+
live: Session, ui: UI, set_live: Callable[[Session], None]
|
|
161
|
+
) -> None:
|
|
162
|
+
set_live(await session.found(live, ui))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def _clipboard(
|
|
166
|
+
live: Session, request: tuple[str, ...], set_live: Callable[[Session], None]
|
|
167
|
+
) -> None:
|
|
168
|
+
set_live(await session.clipped(live, request))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _keep(live: Session) -> None:
|
|
172
|
+
"""The recovery copy covers every open buffer, not only the one on screen."""
|
|
173
|
+
for buffer in live.buffers:
|
|
174
|
+
if not buffer.path:
|
|
175
|
+
continue
|
|
176
|
+
if buffer.modified:
|
|
177
|
+
persist.save_recovery(buffer.path, buffer.lines)
|
|
178
|
+
else:
|
|
179
|
+
persist.drop_recovery(buffer.path)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def opened(paths: list[str]) -> Session:
|
|
183
|
+
"""The session the command line asks for: a buffer per file, the first on screen."""
|
|
184
|
+
buffers = tuple(session.load(path) for path in paths) or (session.load(None),)
|
|
185
|
+
return configured(Session(buffers=buffers))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def main() -> None:
|
|
189
|
+
arguments = [a for a in sys.argv[1:] if not a.startswith("-")]
|
|
190
|
+
if "--help" in sys.argv:
|
|
191
|
+
print(f"usage: turbovi [FILE...]\n\n{HELP}")
|
|
192
|
+
return
|
|
193
|
+
initial = opened(arguments)
|
|
194
|
+
run(lambda ui: app(ui, initial), title="turbovi")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
main()
|