twd-m4sc0 3.0.4__tar.gz → 3.1.1__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.
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/PKG-INFO +1 -1
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/pyproject.toml +5 -1
- twd_m4sc0-3.1.1/src/twd/__init__.py +1 -0
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd/cli.py +90 -5
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd/data.py +33 -8
- twd_m4sc0-3.1.1/src/twd/modals/__init__.py +4 -0
- twd_m4sc0-3.1.1/src/twd/modals/confirm.py +97 -0
- twd_m4sc0-3.1.1/src/twd/modals/edit.py +52 -0
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd/tui.py +86 -17
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd_m4sc0.egg-info/PKG-INFO +1 -1
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd_m4sc0.egg-info/SOURCES.txt +3 -0
- twd_m4sc0-3.0.4/src/twd/__init__.py +0 -6
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/README.md +0 -0
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/setup.cfg +0 -0
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd/config.py +0 -0
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd/tui.tcss +0 -0
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd/utils.py +0 -0
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd_m4sc0.egg-info/dependency_links.txt +0 -0
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd_m4sc0.egg-info/entry_points.txt +0 -0
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd_m4sc0.egg-info/requires.txt +0 -0
- {twd_m4sc0-3.0.4 → twd_m4sc0-3.1.1}/src/twd_m4sc0.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "twd-m4sc0"
|
|
3
|
-
version = "3.0
|
|
3
|
+
# version = "3.1.0"
|
|
4
|
+
dynamic = ["version"]
|
|
4
5
|
description = "TWD by m4sc0"
|
|
5
6
|
authors = [{name = "m4sc0"}]
|
|
6
7
|
requires-python = ">=3.13"
|
|
@@ -32,3 +33,6 @@ where = ["src"]
|
|
|
32
33
|
|
|
33
34
|
[tool.setuptools.package-data]
|
|
34
35
|
twd = ["*.tcss"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.dynamic]
|
|
38
|
+
version = {attr = "twd.__version__"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.1.1"
|
|
@@ -8,7 +8,7 @@ from .tui import TWDApp
|
|
|
8
8
|
|
|
9
9
|
@click.group(invoke_without_command=True)
|
|
10
10
|
@click.pass_context
|
|
11
|
-
@click.version_option(version=__version__)
|
|
11
|
+
@click.version_option(version=__version__, prog_name="twd")
|
|
12
12
|
def cli(ctx):
|
|
13
13
|
"""
|
|
14
14
|
TWD - Temp / Tracked Working Directory
|
|
@@ -39,7 +39,18 @@ def cli(ctx):
|
|
|
39
39
|
@click.pass_context
|
|
40
40
|
def save(ctx, path, alias, name):
|
|
41
41
|
"""
|
|
42
|
-
Save a new
|
|
42
|
+
Save a new TWD with an Alias (UUID like) and Name
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
|
|
46
|
+
twd save ./Movies movies Media
|
|
47
|
+
|
|
48
|
+
twd save / root "Root Directory -- /dev/sda3"
|
|
49
|
+
|
|
50
|
+
twd save . finances-q1 "Finance Documents Q1"
|
|
51
|
+
|
|
52
|
+
The naming conventions for the alias align with kebab-casing.
|
|
53
|
+
There are no naming conventions for the name.
|
|
43
54
|
"""
|
|
44
55
|
manager: TwdManager = ctx.obj['manager']
|
|
45
56
|
|
|
@@ -61,7 +72,13 @@ def save(ctx, path, alias, name):
|
|
|
61
72
|
@click.pass_context
|
|
62
73
|
def get(ctx, alias):
|
|
63
74
|
"""
|
|
64
|
-
|
|
75
|
+
cd into a directory by the given Alias
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
|
|
79
|
+
twd get movies
|
|
80
|
+
|
|
81
|
+
There's really not much to it.
|
|
65
82
|
"""
|
|
66
83
|
manager = ctx.obj['manager']
|
|
67
84
|
|
|
@@ -81,7 +98,13 @@ def get(ctx, alias):
|
|
|
81
98
|
@click.pass_context
|
|
82
99
|
def remove(ctx, alias):
|
|
83
100
|
"""
|
|
84
|
-
|
|
101
|
+
Remove TWD by the given Alias
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
|
|
105
|
+
twd remove movies
|
|
106
|
+
|
|
107
|
+
That's it.
|
|
85
108
|
"""
|
|
86
109
|
manager = ctx.obj['manager']
|
|
87
110
|
|
|
@@ -95,7 +118,9 @@ def remove(ctx, alias):
|
|
|
95
118
|
@cli.command('list')
|
|
96
119
|
@click.pass_context
|
|
97
120
|
def list_twds(ctx):
|
|
98
|
-
"""
|
|
121
|
+
"""
|
|
122
|
+
List all TWDs
|
|
123
|
+
"""
|
|
99
124
|
manager = ctx.obj['manager']
|
|
100
125
|
|
|
101
126
|
entries = manager.list_all()
|
|
@@ -103,5 +128,65 @@ def list_twds(ctx):
|
|
|
103
128
|
click.echo("No TWDs saved yet. Use 'twd save <path> <alias>' to add one.")
|
|
104
129
|
return
|
|
105
130
|
|
|
131
|
+
invalid_found = False
|
|
106
132
|
for entry in entries:
|
|
133
|
+
if not invalid_found and not os.path.exists(entry.path):
|
|
134
|
+
invalid_found = True
|
|
107
135
|
click.echo(f"{entry.alias:20} {entry.name:30} {entry.path}")
|
|
136
|
+
|
|
137
|
+
if invalid_found:
|
|
138
|
+
click.echo(f"\nInvalid TWD paths found. Run <twd clean> to remove them.")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@cli.command('clean')
|
|
142
|
+
@click.option('--yes', '-y', is_flag=True, help="Remove all invalid entries without asking")
|
|
143
|
+
@click.pass_context
|
|
144
|
+
def clean(ctx, yes):
|
|
145
|
+
"""
|
|
146
|
+
Clean the records of rogue TWDs
|
|
147
|
+
|
|
148
|
+
Sometimes it's possible that TWDs are not properly cleaned up.
|
|
149
|
+
That's what this subcommand does.
|
|
150
|
+
"""
|
|
151
|
+
manager = ctx.obj['manager']
|
|
152
|
+
|
|
153
|
+
# all entries, valid and invalid
|
|
154
|
+
entries = manager.list_all()
|
|
155
|
+
|
|
156
|
+
# only invalids
|
|
157
|
+
invalids = []
|
|
158
|
+
for entry in entries:
|
|
159
|
+
if os.path.exists(entry.path):
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
invalids.append(entry)
|
|
163
|
+
|
|
164
|
+
if len(invalids) == 0:
|
|
165
|
+
click.echo("No invalid TWDs found.")
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
click.echo(f"Found {len(invalids)} invalid TWDs\n")
|
|
169
|
+
|
|
170
|
+
# only valid
|
|
171
|
+
valid_entries = []
|
|
172
|
+
for entry in entries:
|
|
173
|
+
if entry not in invalids:
|
|
174
|
+
valid_entries.append(entry)
|
|
175
|
+
|
|
176
|
+
# remove all
|
|
177
|
+
if yes:
|
|
178
|
+
click.echo("Removing all invalid entries...")
|
|
179
|
+
manager._write_all(valid_entries)
|
|
180
|
+
click.echo(f"Done. {len(valid_entries)} TWDs left.")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
to_keep = []
|
|
184
|
+
for inv in invalids:
|
|
185
|
+
if not click.confirm(f"Do you want to remove '{inv.alias}'?", default=True):
|
|
186
|
+
# user wants to keep invalid TWD (weird but i'll allow it)
|
|
187
|
+
to_keep.append(inv)
|
|
188
|
+
|
|
189
|
+
# write a unison list of valid entries and the ones to keep
|
|
190
|
+
final_entries = valid_entries + to_keep
|
|
191
|
+
manager._write_all(final_entries)
|
|
192
|
+
click.echo(f"Done. {len(final_entries)} TWDs left.")
|
|
@@ -15,6 +15,17 @@ class Entry(BaseModel):
|
|
|
15
15
|
name: str = Field(..., min_length=3)
|
|
16
16
|
created_at: datetime = Field(default_factory=datetime.now)
|
|
17
17
|
|
|
18
|
+
def __eq__(self, other) -> bool:
|
|
19
|
+
"""Compare entries based on their values"""
|
|
20
|
+
if not isinstance(other, Entry):
|
|
21
|
+
return NotImplemented
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
self.alias == other.alias
|
|
25
|
+
and self.path == other.path
|
|
26
|
+
and self.name == other.name
|
|
27
|
+
)
|
|
28
|
+
|
|
18
29
|
@validator("alias")
|
|
19
30
|
def validate_alias(cls, v):
|
|
20
31
|
if not v.replace("_", "").replace("-", "").isalnum():
|
|
@@ -26,10 +37,6 @@ class Entry(BaseModel):
|
|
|
26
37
|
def validate_path(cls, v):
|
|
27
38
|
path = Path(v).expanduser().resolve()
|
|
28
39
|
|
|
29
|
-
if not path.exists():
|
|
30
|
-
raise ValueError(f"Path does not exist: {path}")
|
|
31
|
-
if not path.is_dir():
|
|
32
|
-
raise ValueError(f"Path is not a directory: {path}")
|
|
33
40
|
return path
|
|
34
41
|
|
|
35
42
|
def to_csv(self) -> List[str]:
|
|
@@ -51,10 +58,21 @@ class Entry(BaseModel):
|
|
|
51
58
|
created_at=datetime.fromisoformat(row[3])
|
|
52
59
|
)
|
|
53
60
|
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_values(cls, alias, path, name, created_at) -> "Entry":
|
|
63
|
+
"""create from values"""
|
|
64
|
+
return cls(
|
|
65
|
+
alias=alias,
|
|
66
|
+
path=path,
|
|
67
|
+
name=name,
|
|
68
|
+
created_at=created_at
|
|
69
|
+
)
|
|
70
|
+
|
|
54
71
|
class TwdManager:
|
|
55
72
|
"""twd entry manager stored in csv"""
|
|
56
73
|
|
|
57
74
|
CSV_HEADERS = ["alias", "path", "name", "created_at"]
|
|
75
|
+
CSV_HEADERS_FANCY = ["Alias", "Path", "Description", "Created at"]
|
|
58
76
|
|
|
59
77
|
def __init__(self, csv_path: Path):
|
|
60
78
|
self.csv_path = csv_path
|
|
@@ -80,10 +98,7 @@ class TwdManager:
|
|
|
80
98
|
next(reader) # skip headers
|
|
81
99
|
|
|
82
100
|
for row in reader:
|
|
83
|
-
|
|
84
|
-
entries.append(Entry.from_csv(row))
|
|
85
|
-
except Exception as e:
|
|
86
|
-
print(f"Warning: skipping invalid row: {e}")
|
|
101
|
+
entries.append(Entry.from_csv(row))
|
|
87
102
|
|
|
88
103
|
return entries
|
|
89
104
|
|
|
@@ -121,6 +136,16 @@ class TwdManager:
|
|
|
121
136
|
|
|
122
137
|
return None
|
|
123
138
|
|
|
139
|
+
def update(self, alias: str, entry: Entry) -> bool:
|
|
140
|
+
"""update TWD by alias"""
|
|
141
|
+
if not self.exists(alias):
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
# simplest form of update is remove and add
|
|
145
|
+
self.remove(alias)
|
|
146
|
+
|
|
147
|
+
self.add(entry.alias, entry.path, entry.name)
|
|
148
|
+
|
|
124
149
|
def remove(self, alias: str) -> None:
|
|
125
150
|
"""remove entry by alias"""
|
|
126
151
|
entries = self._read_all()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from textual import on
|
|
2
|
+
from textual.app import ComposeResult
|
|
3
|
+
from textual.screen import ModalScreen
|
|
4
|
+
from textual.containers import Container, Horizontal
|
|
5
|
+
from textual.widgets import Button, Label
|
|
6
|
+
from typing import Union, TypeVar, Generic
|
|
7
|
+
|
|
8
|
+
from twd.data import Entry
|
|
9
|
+
|
|
10
|
+
T = TypeVar('T')
|
|
11
|
+
|
|
12
|
+
class ConfirmModal(ModalScreen[T], Generic[T]):
|
|
13
|
+
"""A confirm modal"""
|
|
14
|
+
|
|
15
|
+
DEFAULT_CSS = """
|
|
16
|
+
ConfirmModal {
|
|
17
|
+
align: center middle;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ConfirmModal > Container {
|
|
21
|
+
width: auto;
|
|
22
|
+
height: auto;
|
|
23
|
+
border: thick $background 80%;
|
|
24
|
+
background: $surface;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ConfirmModal > Container > Label {
|
|
28
|
+
width: 100%;
|
|
29
|
+
content-align-horizontal: center;
|
|
30
|
+
margin-top: 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ConfirmModal > Container > Horizontal {
|
|
34
|
+
width: auto;
|
|
35
|
+
height: auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ConfirmModal > Container > Horizontal > Button {
|
|
39
|
+
margin: 2 4;
|
|
40
|
+
}
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
message: Union[str, None] = None,
|
|
46
|
+
confirm_text: str = "Yes",
|
|
47
|
+
cancel_text: str = "No",
|
|
48
|
+
confirm_value: T | None = None,
|
|
49
|
+
cancel_value: T | None = None,
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
message: The message to display when popping the modal
|
|
53
|
+
confirm_text: Text to show for confirmation
|
|
54
|
+
cancel_text: Text to show for cancellation
|
|
55
|
+
"""
|
|
56
|
+
self.message = message
|
|
57
|
+
self.confirm_text = confirm_text
|
|
58
|
+
self.cancel_text = cancel_text
|
|
59
|
+
|
|
60
|
+
self.confirm_value = confirm_value
|
|
61
|
+
self.cancel_value = cancel_value
|
|
62
|
+
|
|
63
|
+
super().__init__()
|
|
64
|
+
|
|
65
|
+
def compose_content(self) -> ComposeResult:
|
|
66
|
+
"""
|
|
67
|
+
Abstract method for presenting custom shenanigans
|
|
68
|
+
"""
|
|
69
|
+
if self.message:
|
|
70
|
+
yield Label(self.message, id="content")
|
|
71
|
+
else:
|
|
72
|
+
yield Label("Are you sure?", id="content")
|
|
73
|
+
|
|
74
|
+
def compose(self) -> ComposeResult:
|
|
75
|
+
with Container():
|
|
76
|
+
yield from self.compose_content()
|
|
77
|
+
with Horizontal():
|
|
78
|
+
yield Button(self.cancel_text, id="cancel", variant="error")
|
|
79
|
+
yield Button(self.confirm_text, id="confirm", variant="success")
|
|
80
|
+
|
|
81
|
+
@on(Button.Pressed, "#cancel")
|
|
82
|
+
def cancel_pressed(self) -> None:
|
|
83
|
+
self.dismiss(self.cancel_value)
|
|
84
|
+
|
|
85
|
+
@on(Button.Pressed, "#confirm")
|
|
86
|
+
def confirm_pressed(self) -> None:
|
|
87
|
+
self.dismiss(self.confirm_value)
|
|
88
|
+
|
|
89
|
+
class EntryDeleteModal(ConfirmModal):
|
|
90
|
+
"""Confirmation modal with detailed entry information"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, entry):
|
|
93
|
+
self.entry = entry
|
|
94
|
+
super().__init__(confirm_text="Delete", cancel_text="Cancel")
|
|
95
|
+
|
|
96
|
+
def compose_content(self) -> ComposeResult:
|
|
97
|
+
yield Label(f"Delete entry '{self.entry.name}'?", id="content")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from textual import on
|
|
2
|
+
from textual.app import ComposeResult
|
|
3
|
+
from textual.containers import Container, Horizontal
|
|
4
|
+
from textual.widgets import Button, Label, Input
|
|
5
|
+
|
|
6
|
+
from twd.data import Entry
|
|
7
|
+
from twd.modals import ConfirmModal
|
|
8
|
+
|
|
9
|
+
class EditEntryModal(ConfirmModal[Entry | None]):
|
|
10
|
+
"""A modal to edit an existing entry"""
|
|
11
|
+
|
|
12
|
+
DEFAULT_CSS = """
|
|
13
|
+
Label {
|
|
14
|
+
width: 100%;
|
|
15
|
+
text-align: left;
|
|
16
|
+
}
|
|
17
|
+
Input {
|
|
18
|
+
margin: 1;
|
|
19
|
+
}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, entry):
|
|
23
|
+
"""
|
|
24
|
+
entry: the entry to edit
|
|
25
|
+
"""
|
|
26
|
+
self.entry = Entry.from_values(entry.alias, entry.path, entry.name, entry.created_at)
|
|
27
|
+
|
|
28
|
+
super().__init__(
|
|
29
|
+
confirm_text="Save",
|
|
30
|
+
cancel_text="Discard",
|
|
31
|
+
confirm_value=self.entry,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def compose_content(self) -> ComposeResult:
|
|
35
|
+
yield Label(f"Edit Entry", id="title")
|
|
36
|
+
|
|
37
|
+
yield Label("Alias")
|
|
38
|
+
yield Input(value=self.entry.alias, id="alias", disabled=True)
|
|
39
|
+
yield Label("Path")
|
|
40
|
+
yield Input(value=str(self.entry.path), id="path")
|
|
41
|
+
yield Label("Name")
|
|
42
|
+
yield Input(value=self.entry.name, id="name")
|
|
43
|
+
|
|
44
|
+
@on(Input.Changed, "#path")
|
|
45
|
+
def on_path_change(self, event: Input.Changed) -> None:
|
|
46
|
+
self.entry.path = event.value
|
|
47
|
+
|
|
48
|
+
@on(Input.Changed, "#name")
|
|
49
|
+
def on_name_change(self, event: Input.Changed) -> None:
|
|
50
|
+
self.entry.name = event.value
|
|
51
|
+
|
|
52
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
from datetime import datetime
|
|
2
3
|
from textual import on
|
|
3
4
|
from textual.app import App, ComposeResult, Binding
|
|
4
5
|
from textual.containers import HorizontalGroup, VerticalScroll
|
|
@@ -7,8 +8,9 @@ from textual.widgets import Button, Digits, Footer, Header, DataTable, Label, Ru
|
|
|
7
8
|
from textual.color import Color
|
|
8
9
|
|
|
9
10
|
from twd.config import Config
|
|
10
|
-
from twd.data import TwdManager
|
|
11
|
+
from twd.data import TwdManager, Entry
|
|
11
12
|
from twd.utils import fuzzy_search, linear_search
|
|
13
|
+
from twd.modals import ConfirmModal, EntryDeleteModal, EditEntryModal
|
|
12
14
|
|
|
13
15
|
class Mode(Enum):
|
|
14
16
|
NORMAL = "normal"
|
|
@@ -27,8 +29,10 @@ class TWDApp(App):
|
|
|
27
29
|
Binding("k", "cursor_up", "Up"),
|
|
28
30
|
|
|
29
31
|
# modify
|
|
30
|
-
Binding("/", "
|
|
31
|
-
Binding("
|
|
32
|
+
Binding("/", "slash_key", "Search"),
|
|
33
|
+
Binding("d", "d_key", "Delete"),
|
|
34
|
+
Binding("e", "e_key", "Edit"),
|
|
35
|
+
Binding("escape", "escape_key", "Normal", show=False),
|
|
32
36
|
# TODO: edit
|
|
33
37
|
# TODO: rename
|
|
34
38
|
|
|
@@ -40,6 +44,7 @@ class TWDApp(App):
|
|
|
40
44
|
]
|
|
41
45
|
|
|
42
46
|
mode: Mode = reactive(Mode.NORMAL)
|
|
47
|
+
search_results = None
|
|
43
48
|
|
|
44
49
|
def __init__(self, manager: TwdManager, *args, **kwargs):
|
|
45
50
|
self.manager = manager
|
|
@@ -77,7 +82,7 @@ class TWDApp(App):
|
|
|
77
82
|
|
|
78
83
|
# add headers
|
|
79
84
|
table = self.query_one(DataTable)
|
|
80
|
-
table.add_columns(*self.manager.
|
|
85
|
+
table.add_columns(*self.manager.CSV_HEADERS_FANCY)
|
|
81
86
|
|
|
82
87
|
self._populate_table()
|
|
83
88
|
|
|
@@ -93,7 +98,20 @@ class TWDApp(App):
|
|
|
93
98
|
|
|
94
99
|
# fill data
|
|
95
100
|
for entry in entries:
|
|
96
|
-
table.add_row(entry.alias, str(entry.path), entry.name, entry.created_at)
|
|
101
|
+
table.add_row(entry.alias, str(entry.path), entry.name, entry.created_at.strftime("%Y-%m-%d %H:%M:%S"))
|
|
102
|
+
|
|
103
|
+
def _current_row_entry(self) -> Entry:
|
|
104
|
+
table = self.query_one(DataTable)
|
|
105
|
+
|
|
106
|
+
# get row
|
|
107
|
+
row_key = table.coordinate_to_cell_key(table.cursor_coordinate).row_key
|
|
108
|
+
row_data = table.get_row(row_key)
|
|
109
|
+
alias = row_data[0]
|
|
110
|
+
|
|
111
|
+
# get entry
|
|
112
|
+
entry = self.manager.get(alias)
|
|
113
|
+
|
|
114
|
+
return entry
|
|
97
115
|
|
|
98
116
|
def watch_mode(self, old_mode: Mode, new_mode: Mode) -> None:
|
|
99
117
|
"""
|
|
@@ -128,8 +146,11 @@ class TWDApp(App):
|
|
|
128
146
|
"""
|
|
129
147
|
table = self.query_one(DataTable)
|
|
130
148
|
|
|
131
|
-
|
|
132
|
-
|
|
149
|
+
try:
|
|
150
|
+
current_row = table.cursor_coordinate.row
|
|
151
|
+
next_row = (current_row + 1) % table.row_count
|
|
152
|
+
except ZeroDivisionError as e:
|
|
153
|
+
return
|
|
133
154
|
|
|
134
155
|
table.move_cursor(row=next_row)
|
|
135
156
|
|
|
@@ -139,12 +160,15 @@ class TWDApp(App):
|
|
|
139
160
|
"""
|
|
140
161
|
table = self.query_one(DataTable)
|
|
141
162
|
|
|
142
|
-
|
|
143
|
-
|
|
163
|
+
try:
|
|
164
|
+
current_row = table.cursor_coordinate.row
|
|
165
|
+
prev_row = (current_row - 1) % table.row_count
|
|
166
|
+
except ZeroDivisionError as e:
|
|
167
|
+
return
|
|
144
168
|
|
|
145
169
|
table.move_cursor(row=prev_row)
|
|
146
170
|
|
|
147
|
-
def
|
|
171
|
+
def action_slash_key(self) -> None:
|
|
148
172
|
"""
|
|
149
173
|
enter search mode
|
|
150
174
|
"""
|
|
@@ -152,14 +176,62 @@ class TWDApp(App):
|
|
|
152
176
|
return
|
|
153
177
|
self.mode = Mode.SEARCH
|
|
154
178
|
|
|
155
|
-
def
|
|
179
|
+
def action_escape_key(self) -> None:
|
|
156
180
|
"""
|
|
157
181
|
enter normal mode
|
|
158
182
|
"""
|
|
159
183
|
if self.mode == Mode.NORMAL:
|
|
184
|
+
if self.search_results is not None:
|
|
185
|
+
self._populate_table()
|
|
186
|
+
self.search_results = None
|
|
160
187
|
return
|
|
161
188
|
self.mode = Mode.NORMAL
|
|
162
189
|
|
|
190
|
+
def action_d_key(self) -> None:
|
|
191
|
+
"""
|
|
192
|
+
open confirm modal and delete entry if yes
|
|
193
|
+
"""
|
|
194
|
+
if not self.mode == Mode.NORMAL:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
entry = self._current_row_entry()
|
|
198
|
+
|
|
199
|
+
def check_delete(decision: bool | None) -> None:
|
|
200
|
+
"""
|
|
201
|
+
open modal and return the decision
|
|
202
|
+
"""
|
|
203
|
+
if not decision:
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
self.manager.remove(alias)
|
|
207
|
+
|
|
208
|
+
self._populate_table()
|
|
209
|
+
|
|
210
|
+
self.notify(f"Removed entry \"{entry.name}\"")
|
|
211
|
+
|
|
212
|
+
self.push_screen(EntryDeleteModal(entry), check_delete)
|
|
213
|
+
|
|
214
|
+
def action_e_key(self) -> None:
|
|
215
|
+
"""
|
|
216
|
+
open edit modal and edit entry in place
|
|
217
|
+
"""
|
|
218
|
+
entry = self._current_row_entry()
|
|
219
|
+
|
|
220
|
+
def save_new_entry(new_entry: Entry | None) -> None:
|
|
221
|
+
if not new_entry or entry == new_entry:
|
|
222
|
+
# user hit 'Discard'
|
|
223
|
+
# no changes so no update
|
|
224
|
+
self.notify(f"No changes")
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
self.notify(f"Updated TWD '{new_entry.alias}'")
|
|
228
|
+
|
|
229
|
+
self.manager.update(new_entry.alias, new_entry)
|
|
230
|
+
self.manager._write_all(self.manager._read_all())
|
|
231
|
+
self._populate_table()
|
|
232
|
+
|
|
233
|
+
self.push_screen(EditEntryModal(entry), save_new_entry)
|
|
234
|
+
|
|
163
235
|
def action_exit(self) -> None:
|
|
164
236
|
self.exit()
|
|
165
237
|
|
|
@@ -186,6 +258,7 @@ class TWDApp(App):
|
|
|
186
258
|
filtered = [item[0] for item in search_result]
|
|
187
259
|
|
|
188
260
|
self._populate_table(filtered)
|
|
261
|
+
self.search_results = filtered
|
|
189
262
|
|
|
190
263
|
@on(Input.Submitted, "#search-input")
|
|
191
264
|
def on_search_submitted(self, e: Input.Submitted) -> None:
|
|
@@ -196,6 +269,7 @@ class TWDApp(App):
|
|
|
196
269
|
return
|
|
197
270
|
|
|
198
271
|
self.mode = Mode.NORMAL
|
|
272
|
+
self._populate_table(self.search_results)
|
|
199
273
|
|
|
200
274
|
self.query_one(DataTable).focus()
|
|
201
275
|
|
|
@@ -208,12 +282,7 @@ class TWDApp(App):
|
|
|
208
282
|
table = event.data_table
|
|
209
283
|
row_key = event.row_key
|
|
210
284
|
|
|
211
|
-
|
|
212
|
-
row_data = table.get_row(row_key)
|
|
213
|
-
alias = row_data[0]
|
|
214
|
-
|
|
215
|
-
# get entry
|
|
216
|
-
entry = self.manager.get(alias)
|
|
285
|
+
entry = self._current_row_entry()
|
|
217
286
|
|
|
218
287
|
self.notify(f"Selected: {entry.alias} -> {entry.path}")
|
|
219
288
|
|
|
@@ -7,6 +7,9 @@ src/twd/data.py
|
|
|
7
7
|
src/twd/tui.py
|
|
8
8
|
src/twd/tui.tcss
|
|
9
9
|
src/twd/utils.py
|
|
10
|
+
src/twd/modals/__init__.py
|
|
11
|
+
src/twd/modals/confirm.py
|
|
12
|
+
src/twd/modals/edit.py
|
|
10
13
|
src/twd_m4sc0.egg-info/PKG-INFO
|
|
11
14
|
src/twd_m4sc0.egg-info/SOURCES.txt
|
|
12
15
|
src/twd_m4sc0.egg-info/dependency_links.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|