commandertui 1.1.0__tar.gz → 1.2.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.
Files changed (50) hide show
  1. {commandertui-1.1.0 → commandertui-1.2.0}/PKG-INFO +53 -7
  2. {commandertui-1.1.0 → commandertui-1.2.0}/README.md +52 -6
  3. commandertui-1.2.0/commandertui/__init__.py +1 -0
  4. commandertui-1.2.0/commandertui/app.py +70 -0
  5. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/bookmarks.py +4 -1
  6. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/cli.py +6 -1
  7. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/compare_screen.py +4 -4
  8. commandertui-1.2.0/commandertui/main_screen.py +443 -0
  9. commandertui-1.2.0/commandertui/modals.py +626 -0
  10. commandertui-1.2.0/commandertui/netlocations.py +118 -0
  11. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/progress_screen.py +38 -3
  12. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/sync.py +13 -2
  13. commandertui-1.2.0/commandertui/theme.py +255 -0
  14. commandertui-1.2.0/commandertui/updater.py +163 -0
  15. commandertui-1.2.0/commandertui/widgets.py +357 -0
  16. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui.egg-info/PKG-INFO +53 -7
  17. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui.egg-info/SOURCES.txt +4 -0
  18. commandertui-1.2.0/tests/test_app_smoke.py +761 -0
  19. commandertui-1.2.0/tests/test_netlocations.py +51 -0
  20. {commandertui-1.1.0 → commandertui-1.2.0}/tests/test_sync.py +19 -0
  21. commandertui-1.2.0/tests/test_updater.py +109 -0
  22. {commandertui-1.1.0 → commandertui-1.2.0}/tests/test_widgets.py +13 -0
  23. commandertui-1.1.0/commandertui/__init__.py +0 -1
  24. commandertui-1.1.0/commandertui/app.py +0 -40
  25. commandertui-1.1.0/commandertui/main_screen.py +0 -211
  26. commandertui-1.1.0/commandertui/modals.py +0 -250
  27. commandertui-1.1.0/commandertui/theme.py +0 -94
  28. commandertui-1.1.0/commandertui/widgets.py +0 -134
  29. commandertui-1.1.0/tests/test_app_smoke.py +0 -129
  30. {commandertui-1.1.0 → commandertui-1.2.0}/LICENSE +0 -0
  31. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/ascii_font.py +0 -0
  32. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/boot.py +0 -0
  33. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/diff.py +0 -0
  34. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/executor.py +0 -0
  35. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/models.py +0 -0
  36. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/plan.py +0 -0
  37. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/run_queue.py +0 -0
  38. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/scanner.py +0 -0
  39. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui/trash.py +0 -0
  40. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui.egg-info/dependency_links.txt +0 -0
  41. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui.egg-info/entry_points.txt +0 -0
  42. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui.egg-info/requires.txt +0 -0
  43. {commandertui-1.1.0 → commandertui-1.2.0}/commandertui.egg-info/top_level.txt +0 -0
  44. {commandertui-1.1.0 → commandertui-1.2.0}/pyproject.toml +0 -0
  45. {commandertui-1.1.0 → commandertui-1.2.0}/setup.cfg +0 -0
  46. {commandertui-1.1.0 → commandertui-1.2.0}/tests/test_bookmarks.py +0 -0
  47. {commandertui-1.1.0 → commandertui-1.2.0}/tests/test_diff.py +0 -0
  48. {commandertui-1.1.0 → commandertui-1.2.0}/tests/test_executor.py +0 -0
  49. {commandertui-1.1.0 → commandertui-1.2.0}/tests/test_plan.py +0 -0
  50. {commandertui-1.1.0 → commandertui-1.2.0}/tests/test_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commandertui
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Dual-pane retro terminal file manager: compare, sync, copy and move folders
5
5
  Author: Humberto Barchini
6
6
  License-Expression: MIT
@@ -22,7 +22,7 @@ Dual-pane terminal file manager: compare two folders, sync them, or move
22
22
  files/folders between them (and to USB/SMB/any mounted path) — with a
23
23
  phosphor-green 80s-terminal look and a short modem-style boot animation.
24
24
 
25
- Status: **v1.1.0** — tests passing. MIT licensed.
25
+ Status: **v1.2.0** — 108/108 tests passing. MIT licensed.
26
26
 
27
27
  ---
28
28
 
@@ -32,7 +32,7 @@ A safe, simple alternative to heavier dual-pane file managers. It was
32
32
  written after an earlier attempt of the same idea had to be scrapped for
33
33
  running an unauthenticated local HTTP server. This project deliberately:
34
34
 
35
- - **Never opens a network port.** Pure terminal UI (Textual), no HTTP server,
35
+ - **Never opens a network port.** Pure terminal UI (Textual), no HTTP server (the only outgoing request is the daily update check, which you can turn off),
36
36
  no webview.
37
37
  - **Never does a raw `rm -rf`.** Deletes always go through the desktop trash
38
38
  (`send2trash` → `gio trash` → manual move into `~/.local/share/Trash`, in
@@ -58,13 +58,19 @@ makepkg -si
58
58
 
59
59
  Remove it with `sudo pacman -R commandertui`.
60
60
 
61
- **Other distros.** It is a regular Python package (Python 3.11+):
61
+ **PyPI (Linux / macOS / WSL).** It is a standard package on [PyPI](https://pypi.org/project/commandertui/) (Python 3.11+). Recommended via `pipx`:
62
62
 
63
63
  ```bash
64
- pipx install git+https://github.com/juniorbarchini-oss/commandertui.git
64
+ pipx install commandertui
65
65
  ```
66
66
 
67
- **macOS.** Same Python package (`pipx install ...` as above), plus
67
+ Or inside an activated virtual environment:
68
+
69
+ ```bash
70
+ pip install commandertui
71
+ ```
72
+
73
+ **macOS.** Same Python package (`pipx install commandertui`), plus
68
74
  [kitty](https://sw.kovidgoyal.net/kitty/) for the standalone window. Places
69
75
  lists the volumes under `/Volumes` (system volumes such as Recovery and
70
76
  Preboot are hidden) and FUSE mounts inside your home folder. To get a
@@ -120,13 +126,17 @@ o.window("^commandertui$", { float = true, center = true, size = { 1174, 637 } }
120
126
  | `↑`/`↓`, `Enter` | Navigate, open directory |
121
127
  | `Backspace` | Go to parent directory |
122
128
  | `Space` | Mark/unmark item under cursor |
129
+ | `F2` | Rename the item under the cursor (never overwrites an existing name) |
123
130
  | `F5` | Copy marked (or item under cursor) to the other pane |
124
131
  | `F6` | Move marked to the other pane |
125
132
  | `F7` | Create a folder in the active pane |
126
133
  | `F8` / `Delete` | Delete marked (to trash) |
134
+ | `/` | Filter the active pane by name (contains, ignores case) as you type; `Enter` keeps it, `Esc` clears it |
135
+ | `+` / `-` / `*` | Mark / unmark by pattern (`*.iso`, or plain text = contains) / invert marks, over the rows shown |
136
+ | `u` | Update: shows the pending new version (see *Updates*) |
127
137
  | `i` | Size: file/folder count and total bytes of marked items (background, `Esc` stops it) |
128
138
  | `h` | Show/hide hidden (dot) files in both panes |
129
- | `p` | Places: jump to a mounted drive (USB/SMB/rclone) or saved bookmark |
139
+ | `p` | Places: jump to a mounted drive (USB/SMB/rclone), saved bookmark or saved network location. Inside: `a` add a network location, `s` save the active pane's folder as a bookmark, `d` delete a saved one |
130
140
  | `c` | Compare: full recursive diff between the two panes' current folders |
131
141
  | `r` | Refresh both panes |
132
142
  | `q` | Quit |
@@ -141,6 +151,41 @@ half-written), and a move never removes a source it didn't fully copy.
141
151
  on-demand mount point doesn't show), GVFS/Nautilus network shares, FUSE
142
152
  mounts inside `$HOME` (e.g. `rclone mount`), and saved bookmarks.
143
153
 
154
+ **Network locations (Linux, needs `gio`/GVFS):** in Places press `a`, pick
155
+ SMB, SFTP or FTP and type the server's IP or name. For SMB the app lists the
156
+ shares on the server and you pick one; SFTP/FTP connect straight in. The
157
+ location is saved in `~/.config/commandertui/network.json` (protocol, host
158
+ and share only -- never a password) and reappears in Places, mounting itself
159
+ when picked. If the server asks for a login, `gio` asks for it in the
160
+ terminal and can remember it in the system keyring, exactly like Nautilus.
161
+
162
+ Every key also has a clickable box on the bottom bar: one row on wide windows
163
+ (168+ columns), two rows when narrower (110 columns is the practical minimum).
164
+ The active pane has the thick border and the inverted path bar; each path bar
165
+ shows the free space of that disk, and the marked count and total size.
166
+
167
+ ## Updates
168
+
169
+ Once a day, in the background and only after the start-up animation, the app
170
+ asks PyPI for the latest version (a plain GET, nothing about you is sent). If
171
+ a newer one exists you get a dialog -- **Update now / Later / Skip this
172
+ version** -- and the header keeps saying `update X available (press u)`.
173
+ It never installs on its own. **Update now** hands the terminal over and runs
174
+ the command that matches how it was installed:
175
+
176
+ | Installed as | Update runs |
177
+ |---|---|
178
+ | Arch package (`makepkg`) | rebuilds from the release's PKGBUILD with `makepkg -si` (your sudo password is typed in the terminal, never read by the app) |
179
+ | pipx | `pipx upgrade commandertui` |
180
+ | pip | `pip install --upgrade commandertui` |
181
+ | git checkout | no button: the dialog tells you to `git pull` |
182
+
183
+ Turn the check off with `commandertui --no-update-check` or by setting
184
+ `COMMANDERTUI_NO_UPDATE_CHECK=1`. `COMMANDERTUI_FAKE_LATEST=9.9.9` shows the
185
+ dialog without a real release, to see how it looks. The logic lives in one
186
+ self-contained file, `commandertui/updater.py`, written to be copied into
187
+ other apps.
188
+
144
189
  Inside **Compare**:
145
190
 
146
191
  | Key | Action |
@@ -167,6 +212,7 @@ commandertui/
167
212
  ├── trash.py # 3-layer safe delete (send2trash / gio trash / manual)
168
213
  ├── executor.py # runs an OperationQueue against the filesystem
169
214
  ├── bookmarks.py # detected mounts (USB/SMB/rclone) + saved places
215
+ ├── netlocations.py # SMB/SFTP/FTP locations via gio (list shares, mount, save)
170
216
  ├── theme.py # green/amber phosphor palettes
171
217
  ├── boot.py # startup "modem" reveal animation
172
218
  ├── widgets.py # FilePanel: one pane's directory listing
@@ -4,7 +4,7 @@ Dual-pane terminal file manager: compare two folders, sync them, or move
4
4
  files/folders between them (and to USB/SMB/any mounted path) — with a
5
5
  phosphor-green 80s-terminal look and a short modem-style boot animation.
6
6
 
7
- Status: **v1.1.0** — tests passing. MIT licensed.
7
+ Status: **v1.2.0** — 108/108 tests passing. MIT licensed.
8
8
 
9
9
  ---
10
10
 
@@ -14,7 +14,7 @@ A safe, simple alternative to heavier dual-pane file managers. It was
14
14
  written after an earlier attempt of the same idea had to be scrapped for
15
15
  running an unauthenticated local HTTP server. This project deliberately:
16
16
 
17
- - **Never opens a network port.** Pure terminal UI (Textual), no HTTP server,
17
+ - **Never opens a network port.** Pure terminal UI (Textual), no HTTP server (the only outgoing request is the daily update check, which you can turn off),
18
18
  no webview.
19
19
  - **Never does a raw `rm -rf`.** Deletes always go through the desktop trash
20
20
  (`send2trash` → `gio trash` → manual move into `~/.local/share/Trash`, in
@@ -40,13 +40,19 @@ makepkg -si
40
40
 
41
41
  Remove it with `sudo pacman -R commandertui`.
42
42
 
43
- **Other distros.** It is a regular Python package (Python 3.11+):
43
+ **PyPI (Linux / macOS / WSL).** It is a standard package on [PyPI](https://pypi.org/project/commandertui/) (Python 3.11+). Recommended via `pipx`:
44
44
 
45
45
  ```bash
46
- pipx install git+https://github.com/juniorbarchini-oss/commandertui.git
46
+ pipx install commandertui
47
47
  ```
48
48
 
49
- **macOS.** Same Python package (`pipx install ...` as above), plus
49
+ Or inside an activated virtual environment:
50
+
51
+ ```bash
52
+ pip install commandertui
53
+ ```
54
+
55
+ **macOS.** Same Python package (`pipx install commandertui`), plus
50
56
  [kitty](https://sw.kovidgoyal.net/kitty/) for the standalone window. Places
51
57
  lists the volumes under `/Volumes` (system volumes such as Recovery and
52
58
  Preboot are hidden) and FUSE mounts inside your home folder. To get a
@@ -102,13 +108,17 @@ o.window("^commandertui$", { float = true, center = true, size = { 1174, 637 } }
102
108
  | `↑`/`↓`, `Enter` | Navigate, open directory |
103
109
  | `Backspace` | Go to parent directory |
104
110
  | `Space` | Mark/unmark item under cursor |
111
+ | `F2` | Rename the item under the cursor (never overwrites an existing name) |
105
112
  | `F5` | Copy marked (or item under cursor) to the other pane |
106
113
  | `F6` | Move marked to the other pane |
107
114
  | `F7` | Create a folder in the active pane |
108
115
  | `F8` / `Delete` | Delete marked (to trash) |
116
+ | `/` | Filter the active pane by name (contains, ignores case) as you type; `Enter` keeps it, `Esc` clears it |
117
+ | `+` / `-` / `*` | Mark / unmark by pattern (`*.iso`, or plain text = contains) / invert marks, over the rows shown |
118
+ | `u` | Update: shows the pending new version (see *Updates*) |
109
119
  | `i` | Size: file/folder count and total bytes of marked items (background, `Esc` stops it) |
110
120
  | `h` | Show/hide hidden (dot) files in both panes |
111
- | `p` | Places: jump to a mounted drive (USB/SMB/rclone) or saved bookmark |
121
+ | `p` | Places: jump to a mounted drive (USB/SMB/rclone), saved bookmark or saved network location. Inside: `a` add a network location, `s` save the active pane's folder as a bookmark, `d` delete a saved one |
112
122
  | `c` | Compare: full recursive diff between the two panes' current folders |
113
123
  | `r` | Refresh both panes |
114
124
  | `q` | Quit |
@@ -123,6 +133,41 @@ half-written), and a move never removes a source it didn't fully copy.
123
133
  on-demand mount point doesn't show), GVFS/Nautilus network shares, FUSE
124
134
  mounts inside `$HOME` (e.g. `rclone mount`), and saved bookmarks.
125
135
 
136
+ **Network locations (Linux, needs `gio`/GVFS):** in Places press `a`, pick
137
+ SMB, SFTP or FTP and type the server's IP or name. For SMB the app lists the
138
+ shares on the server and you pick one; SFTP/FTP connect straight in. The
139
+ location is saved in `~/.config/commandertui/network.json` (protocol, host
140
+ and share only -- never a password) and reappears in Places, mounting itself
141
+ when picked. If the server asks for a login, `gio` asks for it in the
142
+ terminal and can remember it in the system keyring, exactly like Nautilus.
143
+
144
+ Every key also has a clickable box on the bottom bar: one row on wide windows
145
+ (168+ columns), two rows when narrower (110 columns is the practical minimum).
146
+ The active pane has the thick border and the inverted path bar; each path bar
147
+ shows the free space of that disk, and the marked count and total size.
148
+
149
+ ## Updates
150
+
151
+ Once a day, in the background and only after the start-up animation, the app
152
+ asks PyPI for the latest version (a plain GET, nothing about you is sent). If
153
+ a newer one exists you get a dialog -- **Update now / Later / Skip this
154
+ version** -- and the header keeps saying `update X available (press u)`.
155
+ It never installs on its own. **Update now** hands the terminal over and runs
156
+ the command that matches how it was installed:
157
+
158
+ | Installed as | Update runs |
159
+ |---|---|
160
+ | Arch package (`makepkg`) | rebuilds from the release's PKGBUILD with `makepkg -si` (your sudo password is typed in the terminal, never read by the app) |
161
+ | pipx | `pipx upgrade commandertui` |
162
+ | pip | `pip install --upgrade commandertui` |
163
+ | git checkout | no button: the dialog tells you to `git pull` |
164
+
165
+ Turn the check off with `commandertui --no-update-check` or by setting
166
+ `COMMANDERTUI_NO_UPDATE_CHECK=1`. `COMMANDERTUI_FAKE_LATEST=9.9.9` shows the
167
+ dialog without a real release, to see how it looks. The logic lives in one
168
+ self-contained file, `commandertui/updater.py`, written to be copied into
169
+ other apps.
170
+
126
171
  Inside **Compare**:
127
172
 
128
173
  | Key | Action |
@@ -149,6 +194,7 @@ commandertui/
149
194
  ├── trash.py # 3-layer safe delete (send2trash / gio trash / manual)
150
195
  ├── executor.py # runs an OperationQueue against the filesystem
151
196
  ├── bookmarks.py # detected mounts (USB/SMB/rclone) + saved places
197
+ ├── netlocations.py # SMB/SFTP/FTP locations via gio (list shares, mount, save)
152
198
  ├── theme.py # green/amber phosphor palettes
153
199
  ├── boot.py # startup "modem" reveal animation
154
200
  ├── widgets.py # FilePanel: one pane's directory listing
@@ -0,0 +1 @@
1
+ __version__ = "1.2.0"
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from textual.app import App
6
+
7
+ from . import updater
8
+ from .boot import BootScreen
9
+ from .main_screen import MainScreen
10
+ from .theme import css_for
11
+
12
+
13
+ class CommanderApp(App):
14
+ """Owns only the screen stack. All pane behavior lives in MainScreen --
15
+ see its docstring for why that split matters for key bindings.
16
+ """
17
+
18
+ CSS = css_for("green")
19
+
20
+ def __init__(
21
+ self, left_path: str | None = None, right_path: str | None = None, check_updates: bool = True
22
+ ) -> None:
23
+ super().__init__()
24
+ self.check_updates = check_updates and not updater.updates_disabled()
25
+ self.update_info: updater.UpdateInfo | None = None
26
+ self.update_prompted = False
27
+ self.left_path = os.path.abspath(left_path or os.path.expanduser("~"))
28
+ self.right_path = os.path.abspath(right_path or os.path.expanduser("~"))
29
+
30
+ def on_mount(self) -> None:
31
+ self.push_screen(MainScreen(self.left_path, self.right_path))
32
+ self.push_screen(BootScreen(), self._after_boot)
33
+
34
+ def _after_boot(self, _result=None) -> None:
35
+ # Looked up only once the boot animation is over, so a quick answer
36
+ # can never pop a dialog over it.
37
+ if self.check_updates:
38
+ self.run_worker(self._look_for_update, thread=True)
39
+
40
+ def _look_for_update(self) -> None:
41
+ info = updater.check()
42
+ if info:
43
+ self.call_from_thread(self.update_found, info)
44
+
45
+ def update_found(self, info: updater.UpdateInfo) -> None:
46
+ """Remember the newer version and say so in the header; the dialog
47
+ itself waits until the main screen is actually showing (not the boot
48
+ animation)."""
49
+ self.update_info = info
50
+ self.sub_title = f"update {info.latest} available (press u)"
51
+ if isinstance(self.screen, MainScreen):
52
+ self.screen.prompt_update()
53
+
54
+ def clear_update(self) -> None:
55
+ self.update_info = None
56
+ self.sub_title = ""
57
+
58
+ # Convenience passthroughs so existing tests/tools that poke at the app
59
+ # directly (e.g. app._panel("left")) keep working without knowing about
60
+ # the MainScreen split.
61
+ def _panel(self, side: str):
62
+ return self.main_screen._panel(side)
63
+
64
+ @property
65
+ def main_screen(self) -> MainScreen:
66
+ return next(s for s in self.screen_stack if isinstance(s, MainScreen))
67
+
68
+ @property
69
+ def active_side(self) -> str:
70
+ return self.main_screen.active_side
@@ -186,4 +186,7 @@ def remove_bookmark(path: str) -> None:
186
186
 
187
187
  def all_places() -> list[Place]:
188
188
  home = Place(label="Home", path=os.path.expanduser("~"), kind="home")
189
- return [home] + detect_mounted() + detect_network_mounts() + detect_home_mounts() + load_saved()
189
+ from .netlocations import load_locations # lazy: netlocations imports CONFIG_DIR from here
190
+
191
+ saved_net = [Place(label=loc.label, path=loc.uri, kind="net-saved") for loc in load_locations()]
192
+ return [home] + detect_mounted() + detect_network_mounts() + detect_home_mounts() + load_saved() + saved_net
@@ -11,10 +11,15 @@ def main() -> None:
11
11
  parser = argparse.ArgumentParser(prog="commandertui", description="Dual-pane TUI file manager")
12
12
  parser.add_argument("left", nargs="?", default=os.path.expanduser("~"))
13
13
  parser.add_argument("right", nargs="?", default=os.path.expanduser("~"))
14
+ parser.add_argument(
15
+ "--no-update-check",
16
+ action="store_true",
17
+ help="never contact PyPI to look for a newer version",
18
+ )
14
19
  parser.add_argument("--version", action="version", version=f"commandertui {__version__}")
15
20
  args = parser.parse_args()
16
21
 
17
- app = CommanderApp(left_path=args.left, right_path=args.right)
22
+ app = CommanderApp(left_path=args.left, right_path=args.right, check_updates=not args.no_update_check)
18
23
  app.run()
19
24
 
20
25
 
@@ -4,13 +4,14 @@ from textual.app import ComposeResult
4
4
  from textual.binding import Binding
5
5
  from textual.containers import Vertical
6
6
  from textual.screen import Screen
7
- from textual.widgets import DataTable, Footer, Static
7
+ from textual.widgets import DataTable, Static
8
8
 
9
9
  from .diff import compare
10
10
  from .modals import MessageScreen
11
11
  from .models import DiffStatus, OpKind, OpSide, OperationQueue, QueuedOp
12
12
  from .plan import plan_mirror, plan_two_way, plan_update
13
13
  from .run_queue import run_queue_with_progress
14
+ from .widgets import CompareBar
14
15
 
15
16
  STATUS_LABEL = {
16
17
  DiffStatus.LEFT_ONLY: "< only left",
@@ -45,7 +46,7 @@ class CompareScreen(Screen):
45
46
  def compose(self) -> ComposeResult:
46
47
  yield Static("", id="compare-summary")
47
48
  yield DataTable(id="compare-table", cursor_type="row")
48
- yield Footer()
49
+ yield CompareBar()
49
50
 
50
51
  def on_mount(self) -> None:
51
52
  self.reload()
@@ -65,8 +66,7 @@ class CompareScreen(Screen):
65
66
  summary = self.query_one("#compare-summary", Static)
66
67
  summary.update(
67
68
  f"{self.left_root} <-> {self.right_root}\n"
68
- f"{non_identical} differences "
69
- "[m]irror [u]pdate [b]idirectional [space] mark [c]opy marked [esc] back"
69
+ f"{non_identical} differences"
70
70
  )
71
71
 
72
72
  def action_back(self) -> None: