twd-m4sc0 3.1.0__py3-none-any.whl → 3.1.1__py3-none-any.whl

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/__init__.py CHANGED
@@ -1,6 +1 @@
1
- from importlib.metadata import version, PackageNotFoundError
2
-
3
- try:
4
- __version__ = version("twd")
5
- except PackageNotFoundError as e:
6
- __version__ = "unknown"
1
+ __version__ = "3.1.1"
twd/cli.py CHANGED
@@ -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 twd with PATH, [opt] ALIAS and [opt] NAME
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
- get path for ALIAS
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
- remove TWD by ALIAS
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
- """List all TWDs"""
121
+ """
122
+ List all TWDs
123
+ """
99
124
  manager = ctx.obj['manager']
100
125
 
101
126
  entries = manager.list_all()
@@ -118,7 +143,10 @@ def list_twds(ctx):
118
143
  @click.pass_context
119
144
  def clean(ctx, yes):
120
145
  """
121
- clean twds from invalid paths
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.
122
150
  """
123
151
  manager = ctx.obj['manager']
124
152
 
twd/data.py CHANGED
@@ -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():
@@ -47,6 +58,16 @@ class Entry(BaseModel):
47
58
  created_at=datetime.fromisoformat(row[3])
48
59
  )
49
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
+
50
71
  class TwdManager:
51
72
  """twd entry manager stored in csv"""
52
73
 
@@ -115,6 +136,16 @@ class TwdManager:
115
136
 
116
137
  return None
117
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
+
118
149
  def remove(self, alias: str) -> None:
119
150
  """remove entry by alias"""
120
151
  entries = self._read_all()
twd/modals/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from twd.modals.confirm import ConfirmModal, EntryDeleteModal
2
+ from twd.modals.edit import EditEntryModal
3
+
4
+ __all__ = ["ConfirmModal", "EntryDeleteModal", "EditEntryModal"]
@@ -3,11 +3,13 @@ from textual.app import ComposeResult
3
3
  from textual.screen import ModalScreen
4
4
  from textual.containers import Container, Horizontal
5
5
  from textual.widgets import Button, Label
6
- from typing import Union
6
+ from typing import Union, TypeVar, Generic
7
7
 
8
8
  from twd.data import Entry
9
9
 
10
- class ConfirmModal(ModalScreen[bool]):
10
+ T = TypeVar('T')
11
+
12
+ class ConfirmModal(ModalScreen[T], Generic[T]):
11
13
  """A confirm modal"""
12
14
 
13
15
  DEFAULT_CSS = """
@@ -42,7 +44,9 @@ class ConfirmModal(ModalScreen[bool]):
42
44
  self,
43
45
  message: Union[str, None] = None,
44
46
  confirm_text: str = "Yes",
45
- cancel_text: str = "No"
47
+ cancel_text: str = "No",
48
+ confirm_value: T | None = None,
49
+ cancel_value: T | None = None,
46
50
  ):
47
51
  """
48
52
  message: The message to display when popping the modal
@@ -53,6 +57,9 @@ class ConfirmModal(ModalScreen[bool]):
53
57
  self.confirm_text = confirm_text
54
58
  self.cancel_text = cancel_text
55
59
 
60
+ self.confirm_value = confirm_value
61
+ self.cancel_value = cancel_value
62
+
56
63
  super().__init__()
57
64
 
58
65
  def compose_content(self) -> ComposeResult:
@@ -68,18 +75,16 @@ class ConfirmModal(ModalScreen[bool]):
68
75
  with Container():
69
76
  yield from self.compose_content()
70
77
  with Horizontal():
71
- yield Button(self.cancel_text, id="no", variant="error")
72
- yield Button(self.confirm_text, id="yes", variant="success")
73
-
74
- @on(Button.Pressed, "#no")
75
- def no_decision(self) -> None:
76
- """decision no"""
77
- self.dismiss(False)
78
-
79
- @on(Button.Pressed, "#yes")
80
- def yes_decision(self) -> None:
81
- """decision yes"""
82
- self.dismiss(True)
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)
83
88
 
84
89
  class EntryDeleteModal(ConfirmModal):
85
90
  """Confirmation modal with detailed entry information"""
twd/modals/edit.py ADDED
@@ -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
+
twd/tui.py CHANGED
@@ -8,9 +8,9 @@ from textual.widgets import Button, Digits, Footer, Header, DataTable, Label, Ru
8
8
  from textual.color import Color
9
9
 
10
10
  from twd.config import Config
11
- from twd.data import TwdManager
11
+ from twd.data import TwdManager, Entry
12
12
  from twd.utils import fuzzy_search, linear_search
13
- from twd.modals import ConfirmModal, EntryDeleteModal
13
+ from twd.modals import ConfirmModal, EntryDeleteModal, EditEntryModal
14
14
 
15
15
  class Mode(Enum):
16
16
  NORMAL = "normal"
@@ -29,9 +29,10 @@ class TWDApp(App):
29
29
  Binding("k", "cursor_up", "Up"),
30
30
 
31
31
  # modify
32
- Binding("/", "enter_search_mode", "Search"),
33
- Binding("d", "delete_entry", "Delete"),
34
- Binding("escape", "enter_normal_mode", "Normal", show=False),
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),
35
36
  # TODO: edit
36
37
  # TODO: rename
37
38
 
@@ -43,6 +44,7 @@ class TWDApp(App):
43
44
  ]
44
45
 
45
46
  mode: Mode = reactive(Mode.NORMAL)
47
+ search_results = None
46
48
 
47
49
  def __init__(self, manager: TwdManager, *args, **kwargs):
48
50
  self.manager = manager
@@ -98,6 +100,19 @@ class TWDApp(App):
98
100
  for entry in entries:
99
101
  table.add_row(entry.alias, str(entry.path), entry.name, entry.created_at.strftime("%Y-%m-%d %H:%M:%S"))
100
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
115
+
101
116
  def watch_mode(self, old_mode: Mode, new_mode: Mode) -> None:
102
117
  """
103
118
  react to mode changes
@@ -153,7 +168,7 @@ class TWDApp(App):
153
168
 
154
169
  table.move_cursor(row=prev_row)
155
170
 
156
- def action_enter_search_mode(self) -> None:
171
+ def action_slash_key(self) -> None:
157
172
  """
158
173
  enter search mode
159
174
  """
@@ -161,30 +176,25 @@ class TWDApp(App):
161
176
  return
162
177
  self.mode = Mode.SEARCH
163
178
 
164
- def action_enter_normal_mode(self) -> None:
179
+ def action_escape_key(self) -> None:
165
180
  """
166
181
  enter normal mode
167
182
  """
168
183
  if self.mode == Mode.NORMAL:
184
+ if self.search_results is not None:
185
+ self._populate_table()
186
+ self.search_results = None
169
187
  return
170
188
  self.mode = Mode.NORMAL
171
189
 
172
- def action_delete_entry(self) -> None:
190
+ def action_d_key(self) -> None:
173
191
  """
174
192
  open confirm modal and delete entry if yes
175
193
  """
176
194
  if not self.mode == Mode.NORMAL:
177
195
  return
178
196
 
179
- table = self.query_one(DataTable)
180
-
181
- # get row
182
- row_key = table.coordinate_to_cell_key(table.cursor_coordinate).row_key
183
- row_data = table.get_row(row_key)
184
- alias = row_data[0]
185
-
186
- # get entry
187
- entry = self.manager.get(alias)
197
+ entry = self._current_row_entry()
188
198
 
189
199
  def check_delete(decision: bool | None) -> None:
190
200
  """
@@ -199,9 +209,29 @@ class TWDApp(App):
199
209
 
200
210
  self.notify(f"Removed entry \"{entry.name}\"")
201
211
 
202
- # self.push_screen(ConfirmModal(message=f"Are you sure want to delete '{entry.alias}'?"), check_delete)
203
212
  self.push_screen(EntryDeleteModal(entry), check_delete)
204
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
+
205
235
  def action_exit(self) -> None:
206
236
  self.exit()
207
237
 
@@ -228,6 +258,7 @@ class TWDApp(App):
228
258
  filtered = [item[0] for item in search_result]
229
259
 
230
260
  self._populate_table(filtered)
261
+ self.search_results = filtered
231
262
 
232
263
  @on(Input.Submitted, "#search-input")
233
264
  def on_search_submitted(self, e: Input.Submitted) -> None:
@@ -238,6 +269,7 @@ class TWDApp(App):
238
269
  return
239
270
 
240
271
  self.mode = Mode.NORMAL
272
+ self._populate_table(self.search_results)
241
273
 
242
274
  self.query_one(DataTable).focus()
243
275
 
@@ -250,12 +282,7 @@ class TWDApp(App):
250
282
  table = event.data_table
251
283
  row_key = event.row_key
252
284
 
253
- # get row
254
- row_data = table.get_row(row_key)
255
- alias = row_data[0]
256
-
257
- # get entry
258
- entry = self.manager.get(alias)
285
+ entry = self._current_row_entry()
259
286
 
260
287
  self.notify(f"Selected: {entry.alias} -> {entry.path}")
261
288
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twd-m4sc0
3
- Version: 3.1.0
3
+ Version: 3.1.1
4
4
  Summary: TWD by m4sc0
5
5
  Author: m4sc0
6
6
  License: MIT
@@ -0,0 +1,15 @@
1
+ twd/__init__.py,sha256=14eImCCNxRh4pWMIfkKe4h5OCS1ICfRjHSj2AfgEXa0,22
2
+ twd/cli.py,sha256=jRdFB741czlkGn9bUrUy4cV9B6iae-87vJpTCcKp04Y,4824
3
+ twd/config.py,sha256=pGAaurguz1Bv6NxjwKbLBbvJ9tuAVJx_l7tcvd0B84I,1996
4
+ twd/data.py,sha256=_Wn4aoJsYyFmrjtuzOou2x70YH4ZuJrGnIdflzjIttc,4756
5
+ twd/tui.py,sha256=X8HWrtvru8nigKx61aMemQ-elJM8UrUZdhjTXMZBdF0,8253
6
+ twd/tui.tcss,sha256=GVEtk-fUcAaaGP3I-VjTFUzZocPAEcASDCBNGVoUBWw,778
7
+ twd/utils.py,sha256=xsMa69vAotpNMd3Vp5_F7ELwZCwT-sd45taN7PRI9pE,1201
8
+ twd/modals/__init__.py,sha256=PE3zZ9RrWABlvjoBRnpbbNj8t7xXy24mqLeW3uVv7Yg,171
9
+ twd/modals/confirm.py,sha256=QOEnV6OYzt95ZWrTd4lTULfWd1bl52aotZWu4uBovsk,2814
10
+ twd/modals/edit.py,sha256=c7Mr5cNeq5XR6EdHCzbWi32ruCPHXDNIZPgEv8cYtj4,1475
11
+ twd_m4sc0-3.1.1.dist-info/METADATA,sha256=BYD3Mv8S298thP3dzitSMEr2FdPtFHw5PkRLsjQapI0,437
12
+ twd_m4sc0-3.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
13
+ twd_m4sc0-3.1.1.dist-info/entry_points.txt,sha256=KeIDpaQx0GSqgJFYql14wmT3yj_JHTwq7rRsm-mKckc,36
14
+ twd_m4sc0-3.1.1.dist-info/top_level.txt,sha256=B93Li7fIZ4uWoKq8rKVuGPU_MtzyjxxDb0NF96oGZ0o,4
15
+ twd_m4sc0-3.1.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,13 +0,0 @@
1
- twd/__init__.py,sha256=76brvw44gXGskDSsajfwk5rvxoRjk4PT99d4qGUs_64,162
2
- twd/cli.py,sha256=m1Buh3_pDg4Yej-3v2cRlB2m-1jz55cLnPx63TJgb4s,4247
3
- twd/config.py,sha256=pGAaurguz1Bv6NxjwKbLBbvJ9tuAVJx_l7tcvd0B84I,1996
4
- twd/data.py,sha256=8GmBk5VOfmvJJa16H2jywi06_0wRNYjjkseqgM17Hdk,3861
5
- twd/modals.py,sha256=NWeOu87DlVxYERUqKfulxRXlb1Bs1BYxs_Q8tih-WZM,2601
6
- twd/tui.py,sha256=XksMYTDgOL5sZg4Cn0LjnNVYvOkgKHsUlve1KZZNmyo,7403
7
- twd/tui.tcss,sha256=GVEtk-fUcAaaGP3I-VjTFUzZocPAEcASDCBNGVoUBWw,778
8
- twd/utils.py,sha256=xsMa69vAotpNMd3Vp5_F7ELwZCwT-sd45taN7PRI9pE,1201
9
- twd_m4sc0-3.1.0.dist-info/METADATA,sha256=ZzuIkcbhHTraVKyx3u_aNymjTYxQv8Fl577mjuiozpE,437
10
- twd_m4sc0-3.1.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
11
- twd_m4sc0-3.1.0.dist-info/entry_points.txt,sha256=KeIDpaQx0GSqgJFYql14wmT3yj_JHTwq7rRsm-mKckc,36
12
- twd_m4sc0-3.1.0.dist-info/top_level.txt,sha256=B93Li7fIZ4uWoKq8rKVuGPU_MtzyjxxDb0NF96oGZ0o,4
13
- twd_m4sc0-3.1.0.dist-info/RECORD,,