twd-m4sc0 3.0.3__py3-none-any.whl → 3.1.0__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/cli.py CHANGED
@@ -24,10 +24,13 @@ def cli(ctx):
24
24
 
25
25
  path = TWDApp(manager=ctx.obj['manager']).run()
26
26
 
27
+ if not path:
28
+ print("Exiting...")
29
+ exit(0)
30
+
27
31
  # write to fd3
28
32
  os.write(3, bytes(str(path), "utf-8"))
29
-
30
- pass
33
+ exit(0)
31
34
 
32
35
  @cli.command()
33
36
  @click.argument('path')
@@ -100,5 +103,62 @@ def list_twds(ctx):
100
103
  click.echo("No TWDs saved yet. Use 'twd save <path> <alias>' to add one.")
101
104
  return
102
105
 
106
+ invalid_found = False
103
107
  for entry in entries:
108
+ if not invalid_found and not os.path.exists(entry.path):
109
+ invalid_found = True
104
110
  click.echo(f"{entry.alias:20} {entry.name:30} {entry.path}")
111
+
112
+ if invalid_found:
113
+ click.echo(f"\nInvalid TWD paths found. Run <twd clean> to remove them.")
114
+
115
+
116
+ @cli.command('clean')
117
+ @click.option('--yes', '-y', is_flag=True, help="Remove all invalid entries without asking")
118
+ @click.pass_context
119
+ def clean(ctx, yes):
120
+ """
121
+ clean twds from invalid paths
122
+ """
123
+ manager = ctx.obj['manager']
124
+
125
+ # all entries, valid and invalid
126
+ entries = manager.list_all()
127
+
128
+ # only invalids
129
+ invalids = []
130
+ for entry in entries:
131
+ if os.path.exists(entry.path):
132
+ continue
133
+
134
+ invalids.append(entry)
135
+
136
+ if len(invalids) == 0:
137
+ click.echo("No invalid TWDs found.")
138
+ return
139
+
140
+ click.echo(f"Found {len(invalids)} invalid TWDs\n")
141
+
142
+ # only valid
143
+ valid_entries = []
144
+ for entry in entries:
145
+ if entry not in invalids:
146
+ valid_entries.append(entry)
147
+
148
+ # remove all
149
+ if yes:
150
+ click.echo("Removing all invalid entries...")
151
+ manager._write_all(valid_entries)
152
+ click.echo(f"Done. {len(valid_entries)} TWDs left.")
153
+ return
154
+
155
+ to_keep = []
156
+ for inv in invalids:
157
+ if not click.confirm(f"Do you want to remove '{inv.alias}'?", default=True):
158
+ # user wants to keep invalid TWD (weird but i'll allow it)
159
+ to_keep.append(inv)
160
+
161
+ # write a unison list of valid entries and the ones to keep
162
+ final_entries = valid_entries + to_keep
163
+ manager._write_all(final_entries)
164
+ click.echo(f"Done. {len(final_entries)} TWDs left.")
twd/data.py CHANGED
@@ -6,7 +6,6 @@ from typing import List, Optional
6
6
  from datetime import datetime
7
7
 
8
8
  from .config import Config
9
- from .utils import search
10
9
 
11
10
  class Entry(BaseModel):
12
11
  """Data class for a signle TWD Entry"""
@@ -27,10 +26,6 @@ class Entry(BaseModel):
27
26
  def validate_path(cls, v):
28
27
  path = Path(v).expanduser().resolve()
29
28
 
30
- if not path.exists():
31
- raise ValueError(f"Path does not exist: {path}")
32
- if not path.is_dir():
33
- raise ValueError(f"Path is not a directory: {path}")
34
29
  return path
35
30
 
36
31
  def to_csv(self) -> List[str]:
@@ -56,6 +51,7 @@ class TwdManager:
56
51
  """twd entry manager stored in csv"""
57
52
 
58
53
  CSV_HEADERS = ["alias", "path", "name", "created_at"]
54
+ CSV_HEADERS_FANCY = ["Alias", "Path", "Description", "Created at"]
59
55
 
60
56
  def __init__(self, csv_path: Path):
61
57
  self.csv_path = csv_path
@@ -81,10 +77,7 @@ class TwdManager:
81
77
  next(reader) # skip headers
82
78
 
83
79
  for row in reader:
84
- try:
85
- entries.append(Entry.from_csv(row))
86
- except Exception as e:
87
- print(f"Warning: skipping invalid row: {e}")
80
+ entries.append(Entry.from_csv(row))
88
81
 
89
82
  return entries
90
83
 
twd/modals.py ADDED
@@ -0,0 +1,92 @@
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
7
+
8
+ from twd.data import Entry
9
+
10
+ class ConfirmModal(ModalScreen[bool]):
11
+ """A confirm modal"""
12
+
13
+ DEFAULT_CSS = """
14
+ ConfirmModal {
15
+ align: center middle;
16
+ }
17
+
18
+ ConfirmModal > Container {
19
+ width: auto;
20
+ height: auto;
21
+ border: thick $background 80%;
22
+ background: $surface;
23
+ }
24
+
25
+ ConfirmModal > Container > Label {
26
+ width: 100%;
27
+ content-align-horizontal: center;
28
+ margin-top: 1;
29
+ }
30
+
31
+ ConfirmModal > Container > Horizontal {
32
+ width: auto;
33
+ height: auto;
34
+ }
35
+
36
+ ConfirmModal > Container > Horizontal > Button {
37
+ margin: 2 4;
38
+ }
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ message: Union[str, None] = None,
44
+ confirm_text: str = "Yes",
45
+ cancel_text: str = "No"
46
+ ):
47
+ """
48
+ message: The message to display when popping the modal
49
+ confirm_text: Text to show for confirmation
50
+ cancel_text: Text to show for cancellation
51
+ """
52
+ self.message = message
53
+ self.confirm_text = confirm_text
54
+ self.cancel_text = cancel_text
55
+
56
+ super().__init__()
57
+
58
+ def compose_content(self) -> ComposeResult:
59
+ """
60
+ Abstract method for presenting custom shenanigans
61
+ """
62
+ if self.message:
63
+ yield Label(self.message, id="content")
64
+ else:
65
+ yield Label("Are you sure?", id="content")
66
+
67
+ def compose(self) -> ComposeResult:
68
+ with Container():
69
+ yield from self.compose_content()
70
+ 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)
83
+
84
+ class EntryDeleteModal(ConfirmModal):
85
+ """Confirmation modal with detailed entry information"""
86
+
87
+ def __init__(self, entry):
88
+ self.entry = entry
89
+ super().__init__(confirm_text="Delete", cancel_text="Cancel")
90
+
91
+ def compose_content(self) -> ComposeResult:
92
+ yield Label(f"Delete entry '{self.entry.name}'?", id="content")
twd/tui.py CHANGED
@@ -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
@@ -8,7 +9,8 @@ from textual.color import Color
8
9
 
9
10
  from twd.config import Config
10
11
  from twd.data import TwdManager
11
- from twd.utils import search, linear_search
12
+ from twd.utils import fuzzy_search, linear_search
13
+ from twd.modals import ConfirmModal, EntryDeleteModal
12
14
 
13
15
  class Mode(Enum):
14
16
  NORMAL = "normal"
@@ -28,6 +30,7 @@ class TWDApp(App):
28
30
 
29
31
  # modify
30
32
  Binding("/", "enter_search_mode", "Search"),
33
+ Binding("d", "delete_entry", "Delete"),
31
34
  Binding("escape", "enter_normal_mode", "Normal", show=False),
32
35
  # TODO: edit
33
36
  # TODO: rename
@@ -77,7 +80,7 @@ class TWDApp(App):
77
80
 
78
81
  # add headers
79
82
  table = self.query_one(DataTable)
80
- table.add_columns(*self.manager.CSV_HEADERS)
83
+ table.add_columns(*self.manager.CSV_HEADERS_FANCY)
81
84
 
82
85
  self._populate_table()
83
86
 
@@ -93,7 +96,7 @@ class TWDApp(App):
93
96
 
94
97
  # fill data
95
98
  for entry in entries:
96
- table.add_row(entry.alias, str(entry.path), entry.name, entry.created_at)
99
+ table.add_row(entry.alias, str(entry.path), entry.name, entry.created_at.strftime("%Y-%m-%d %H:%M:%S"))
97
100
 
98
101
  def watch_mode(self, old_mode: Mode, new_mode: Mode) -> None:
99
102
  """
@@ -128,8 +131,11 @@ class TWDApp(App):
128
131
  """
129
132
  table = self.query_one(DataTable)
130
133
 
131
- current_row = table.cursor_coordinate.row
132
- next_row = (current_row + 1) % table.row_count
134
+ try:
135
+ current_row = table.cursor_coordinate.row
136
+ next_row = (current_row + 1) % table.row_count
137
+ except ZeroDivisionError as e:
138
+ return
133
139
 
134
140
  table.move_cursor(row=next_row)
135
141
 
@@ -139,8 +145,11 @@ class TWDApp(App):
139
145
  """
140
146
  table = self.query_one(DataTable)
141
147
 
142
- current_row = table.cursor_coordinate.row
143
- prev_row = (current_row - 1) % table.row_count
148
+ try:
149
+ current_row = table.cursor_coordinate.row
150
+ prev_row = (current_row - 1) % table.row_count
151
+ except ZeroDivisionError as e:
152
+ return
144
153
 
145
154
  table.move_cursor(row=prev_row)
146
155
 
@@ -160,6 +169,39 @@ class TWDApp(App):
160
169
  return
161
170
  self.mode = Mode.NORMAL
162
171
 
172
+ def action_delete_entry(self) -> None:
173
+ """
174
+ open confirm modal and delete entry if yes
175
+ """
176
+ if not self.mode == Mode.NORMAL:
177
+ return
178
+
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)
188
+
189
+ def check_delete(decision: bool | None) -> None:
190
+ """
191
+ open modal and return the decision
192
+ """
193
+ if not decision:
194
+ return
195
+
196
+ self.manager.remove(alias)
197
+
198
+ self._populate_table()
199
+
200
+ self.notify(f"Removed entry \"{entry.name}\"")
201
+
202
+ # self.push_screen(ConfirmModal(message=f"Are you sure want to delete '{entry.alias}'?"), check_delete)
203
+ self.push_screen(EntryDeleteModal(entry), check_delete)
204
+
163
205
  def action_exit(self) -> None:
164
206
  self.exit()
165
207
 
@@ -177,9 +219,13 @@ class TWDApp(App):
177
219
 
178
220
  # TODO: filter entries and repopulate table
179
221
 
180
- search_result = linear_search(query, all_entries)
222
+ if query is None:
223
+ self._populate_table()
224
+ return
225
+
226
+ search_result = fuzzy_search(query, all_entries)
181
227
 
182
- filtered = [entry for entry in all_entries if entry.alias in search_result]
228
+ filtered = [item[0] for item in search_result]
183
229
 
184
230
  self._populate_table(filtered)
185
231
 
twd/utils.py CHANGED
@@ -1,39 +1,40 @@
1
1
  from rapidfuzz import fuzz
2
2
  from typing import List
3
3
 
4
- FUZZY_THRESHOLD = 50
5
-
6
4
  def normalize(name) -> str:
7
5
  return name.lower().replace('-', ' ').replace('_', ' ')
8
6
 
9
- def search(query, items, threshold = 50) -> List:
7
+ def fuzzy_search(query, items, threshold = 50) -> List:
10
8
  """
11
9
  query: search input
12
10
  items: list of (alias, name)
11
+ threshold: threshold score over which the entries are selected
13
12
 
14
- returns: list of (alias, name, score)
13
+ returns: list of (entry, score)
15
14
  """
16
15
 
17
16
  # return all items if query is empty
18
17
  if not query:
19
- return [(alias, name, 100) for alias, name in items]
18
+ return [(e, 100) for e in items]
20
19
 
21
20
  normalized_query = normalize(query)
22
21
  results = []
23
22
 
24
23
  # filtering
25
- for alias, name in items:
26
- alias_score = fuzz.WRatio(normalized_query, normalize(alias))
27
- name_score = fuzz.WRatio(normalized_query, normalize(name))
24
+ for entry in items:
25
+ alias, name = normalize(entry.alias), normalize(entry.name)
26
+
27
+ alias_score = fuzz.ratio(normalized_query, alias)
28
+ name_score = fuzz.ratio(normalized_query, name)
28
29
 
29
30
  # choose higher score
30
31
  best_score = max(alias_score, name_score)
31
32
 
32
33
  # filter out low scores
33
- if best_score >= threshold:
34
- results.append((alias, name, best_score))
34
+ if best_score > threshold:
35
+ results.append((entry, best_score))
35
36
 
36
- results.sort(key=lambda x: x[2], reverse=True)
37
+ results.sort(key=lambda x: x[1], reverse=True)
37
38
 
38
39
  return results
39
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: twd-m4sc0
3
- Version: 3.0.3
3
+ Version: 3.1.0
4
4
  Summary: TWD by m4sc0
5
5
  Author: m4sc0
6
6
  License: MIT
@@ -0,0 +1,13 @@
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,12 +0,0 @@
1
- twd/__init__.py,sha256=76brvw44gXGskDSsajfwk5rvxoRjk4PT99d4qGUs_64,162
2
- twd/cli.py,sha256=Shm8ZeHrPKPr-lbo6Mvb_mjWEH8mYBVlpvZ3OcWb2sw,2550
3
- twd/config.py,sha256=pGAaurguz1Bv6NxjwKbLBbvJ9tuAVJx_l7tcvd0B84I,1996
4
- twd/data.py,sha256=YPVU2NhZuDOps-pkquRG9y4GW5RHf46eXIm23rgY_Lo,4131
5
- twd/tui.py,sha256=2SGGQ5EhjZSun6NrOiU9aC6AQWfjWEZzFZjKlcIQ9vU,6062
6
- twd/tui.tcss,sha256=GVEtk-fUcAaaGP3I-VjTFUzZocPAEcASDCBNGVoUBWw,778
7
- twd/utils.py,sha256=F42JxIlqUMLsklQ7_zXoIOPmh872E2pHdu1jGLt6-vw,1144
8
- twd_m4sc0-3.0.3.dist-info/METADATA,sha256=IB8U5YkNDs3MldgdSy6ffadSt84fFpgT15yz3QIjktg,437
9
- twd_m4sc0-3.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- twd_m4sc0-3.0.3.dist-info/entry_points.txt,sha256=KeIDpaQx0GSqgJFYql14wmT3yj_JHTwq7rRsm-mKckc,36
11
- twd_m4sc0-3.0.3.dist-info/top_level.txt,sha256=B93Li7fIZ4uWoKq8rKVuGPU_MtzyjxxDb0NF96oGZ0o,4
12
- twd_m4sc0-3.0.3.dist-info/RECORD,,