twd-m4sc0 2.0.3__py3-none-any.whl → 3.0.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 +6 -0
- twd/cli.py +104 -0
- twd/config.py +76 -0
- twd/data.py +143 -0
- twd/tui.py +217 -0
- twd/utils.py +46 -0
- twd_m4sc0-3.0.1.dist-info/METADATA +14 -0
- twd_m4sc0-3.0.1.dist-info/RECORD +11 -0
- {twd_m4sc0-2.0.3.dist-info → twd_m4sc0-3.0.1.dist-info}/WHEEL +1 -1
- twd_m4sc0-3.0.1.dist-info/entry_points.txt +2 -0
- twd_m4sc0-3.0.1.dist-info/top_level.txt +1 -0
- tests/__init__.py +0 -0
- tests/test_twd.py +0 -20
- twd/__main__.py +0 -4
- twd/crud.py +0 -104
- twd/logger.py +0 -47
- twd/screen.py +0 -201
- twd/twd.py +0 -361
- twd_m4sc0-2.0.3.dist-info/LICENSE +0 -21
- twd_m4sc0-2.0.3.dist-info/METADATA +0 -179
- twd_m4sc0-2.0.3.dist-info/RECORD +0 -14
- twd_m4sc0-2.0.3.dist-info/entry_points.txt +0 -2
- twd_m4sc0-2.0.3.dist-info/top_level.txt +0 -2
twd/crud.py
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
import hashlib
|
|
4
|
-
import time
|
|
5
|
-
import logging
|
|
6
|
-
from collections import OrderedDict
|
|
7
|
-
|
|
8
|
-
log = logging.getLogger("log")
|
|
9
|
-
error_log = logging.getLogger("error")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def create_alias_id():
|
|
13
|
-
data = str(time.time()) + str(os.urandom(16))
|
|
14
|
-
return hashlib.sha256(data.encode()).hexdigest()[:12]
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def get_data_file(config):
|
|
18
|
-
return os.path.expanduser(config.get("data_file", "~/.twd/data"))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def ensure_data_file_exists(config):
|
|
22
|
-
data_file = get_data_file(config)
|
|
23
|
-
if not os.path.exists(data_file):
|
|
24
|
-
try:
|
|
25
|
-
with open(data_file, "w") as f:
|
|
26
|
-
json.dump({}, f)
|
|
27
|
-
log.info(f"Created data file at {data_file}")
|
|
28
|
-
except OSError as e:
|
|
29
|
-
error_log.error(f"Error creating data file: {e}")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def load_data(config):
|
|
33
|
-
data_file = get_data_file(config)
|
|
34
|
-
if not os.path.exists(data_file):
|
|
35
|
-
ensure_data_file_exists(config)
|
|
36
|
-
try:
|
|
37
|
-
with open(data_file, "r") as f:
|
|
38
|
-
data = json.load(f)
|
|
39
|
-
log.info(f"Loaded data from {data_file}")
|
|
40
|
-
return data
|
|
41
|
-
except json.JSONDecodeError as e:
|
|
42
|
-
error_log.error(f"Error reading data file: {e}")
|
|
43
|
-
return {}
|
|
44
|
-
except OSError as e:
|
|
45
|
-
error_log.error(f"Error reading data file: {e}")
|
|
46
|
-
return {}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def save_data(config, data):
|
|
50
|
-
data_file = get_data_file(config)
|
|
51
|
-
try:
|
|
52
|
-
sorted_data = OrderedDict(
|
|
53
|
-
sorted(data.items(), key=lambda item: item[1]["alias"])
|
|
54
|
-
)
|
|
55
|
-
with open(data_file, "w") as f:
|
|
56
|
-
json.dump(sorted_data, f, indent=4)
|
|
57
|
-
log.info(f"Saved data to {data_file}")
|
|
58
|
-
except OSError as e:
|
|
59
|
-
error_log.error(f"Error writing to data file: {e}")
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def create_entry(config, data, path, alias=None):
|
|
63
|
-
alias_id = create_alias_id()
|
|
64
|
-
data[alias_id] = {
|
|
65
|
-
"path": path,
|
|
66
|
-
"alias": alias if alias else "no_alias",
|
|
67
|
-
"created_at": time.time(),
|
|
68
|
-
}
|
|
69
|
-
save_data(config, data)
|
|
70
|
-
log.info(f"Created new entry with alias_id '{alias_id}' and path '{path}'")
|
|
71
|
-
return alias_id
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def delete_entry(config, data, entry_id):
|
|
75
|
-
if entry_id in data:
|
|
76
|
-
del data[entry_id]
|
|
77
|
-
save_data(config, data)
|
|
78
|
-
log.info(f"Deleted entry with alias_id '{entry_id}'")
|
|
79
|
-
else:
|
|
80
|
-
error_log.error(f"Entry ID '{entry_id}' not found")
|
|
81
|
-
raise KeyError(f"Entry ID {entry_id} not found")
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def update_entry(config, data, entry_id, entry):
|
|
85
|
-
if entry_id in data:
|
|
86
|
-
data[entry_id] = entry
|
|
87
|
-
save_data(config, data)
|
|
88
|
-
log.info(f"Updated entry with alias_id '{entry_id}'")
|
|
89
|
-
else:
|
|
90
|
-
error_log.error(f"Entry ID '{entry_id}' not found")
|
|
91
|
-
raise KeyError(f"Entry ID {entry_id} not found")
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def delete_data_file(config):
|
|
95
|
-
data_file = get_data_file(config)
|
|
96
|
-
if os.path.exists(data_file):
|
|
97
|
-
try:
|
|
98
|
-
os.remove(data_file)
|
|
99
|
-
log.info(f"Deleted data file at {data_file}")
|
|
100
|
-
except OSError as e:
|
|
101
|
-
error_log.error(f"Error deleting data file: {e}")
|
|
102
|
-
raise
|
|
103
|
-
else:
|
|
104
|
-
error_log.error("No data file found to delete")
|
twd/logger.py
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import os
|
|
3
|
-
from logging.handlers import RotatingFileHandler
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def setup_logger(config):
|
|
7
|
-
"""Set up loggers based on the configuration provided."""
|
|
8
|
-
log_file = os.path.expanduser(config.get("log_file"))
|
|
9
|
-
error_file = os.path.expanduser(config.get("error_file"))
|
|
10
|
-
log_level = config.get("log_level", "INFO").upper()
|
|
11
|
-
max_bytes = config.get("log_max_bytes", 5 * 1024 * 1024) # Default 5MB
|
|
12
|
-
backup_count = config.get("log_backup_count", 3) # Default 3 backup files
|
|
13
|
-
|
|
14
|
-
os.makedirs(os.path.dirname(log_file), exist_ok=True)
|
|
15
|
-
os.makedirs(os.path.dirname(error_file), exist_ok=True)
|
|
16
|
-
|
|
17
|
-
# General log configuration
|
|
18
|
-
log_formatter = logging.Formatter(
|
|
19
|
-
config.get("log_format", "%(asctime)s - %(levelname)s - %(message)s")
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
# Avoid duplicate handlers
|
|
23
|
-
logger = logging.getLogger("log")
|
|
24
|
-
if not logger.hasHandlers():
|
|
25
|
-
logger.setLevel(log_level)
|
|
26
|
-
log_handler = RotatingFileHandler(
|
|
27
|
-
log_file, maxBytes=max_bytes, backupCount=backup_count
|
|
28
|
-
)
|
|
29
|
-
log_handler.setFormatter(log_formatter)
|
|
30
|
-
logger.addHandler(log_handler)
|
|
31
|
-
|
|
32
|
-
# Error log configuration
|
|
33
|
-
error_logger = logging.getLogger("error")
|
|
34
|
-
if not error_logger.hasHandlers():
|
|
35
|
-
error_logger.setLevel(logging.ERROR)
|
|
36
|
-
error_handler = RotatingFileHandler(
|
|
37
|
-
error_file, maxBytes=max_bytes, backupCount=backup_count
|
|
38
|
-
)
|
|
39
|
-
error_handler.setFormatter(log_formatter)
|
|
40
|
-
error_logger.addHandler(error_handler)
|
|
41
|
-
|
|
42
|
-
return logger, error_logger
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def initialize_logging(config):
|
|
46
|
-
setup_logger(config)
|
|
47
|
-
|
twd/screen.py
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import curses
|
|
2
|
-
import time
|
|
3
|
-
import os
|
|
4
|
-
from . import crud
|
|
5
|
-
import logging
|
|
6
|
-
|
|
7
|
-
log = logging.getLogger("log")
|
|
8
|
-
error_log = logging.getLogger("error")
|
|
9
|
-
|
|
10
|
-
CONFIG = None
|
|
11
|
-
DIRS = None
|
|
12
|
-
filtered_DIRS = None
|
|
13
|
-
search_query = ""
|
|
14
|
-
original_DIRS = None
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def draw_hr(stdscr, y, mode=None):
|
|
18
|
-
_, max_cols = stdscr.getmaxyx()
|
|
19
|
-
mode = mode if mode is not None else curses.A_NORMAL
|
|
20
|
-
stdscr.addstr(y, 1, "─" * (max_cols - 2), mode)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def filter_dirs_by_search(query):
|
|
24
|
-
global filtered_DIRS
|
|
25
|
-
filtered_DIRS = (
|
|
26
|
-
{k: v for k, v in DIRS.items() if query.lower() in v["alias"].lower()}
|
|
27
|
-
if query
|
|
28
|
-
else DIRS
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def display_select_screen(stdscr):
|
|
33
|
-
global search_query, filtered_DIRS, original_DIRS
|
|
34
|
-
selected_entry = 0
|
|
35
|
-
pre_selected_path = None
|
|
36
|
-
confirm_mode = False
|
|
37
|
-
action = None
|
|
38
|
-
search_mode = False
|
|
39
|
-
post_search_mode = False
|
|
40
|
-
|
|
41
|
-
running = True
|
|
42
|
-
|
|
43
|
-
while running:
|
|
44
|
-
max_items = len(filtered_DIRS)
|
|
45
|
-
stdscr.clear()
|
|
46
|
-
|
|
47
|
-
# Border setup
|
|
48
|
-
height, width = stdscr.getmaxyx()
|
|
49
|
-
stdscr.addstr(0, 0, "╭")
|
|
50
|
-
stdscr.addstr(0, 1, "─" * (width - 2))
|
|
51
|
-
stdscr.addstr(0, width - 1, "╮")
|
|
52
|
-
stdscr.addstr(height - 1, 0, "╰")
|
|
53
|
-
stdscr.addstr(height - 1, 1, "─" * (width - 2))
|
|
54
|
-
stdscr.addstr(height - 2, width - 1, "╯")
|
|
55
|
-
for i in range(1, height - 1):
|
|
56
|
-
stdscr.addstr(i, 0, "│")
|
|
57
|
-
stdscr.addstr(i, width - 1, "│")
|
|
58
|
-
|
|
59
|
-
inner_height = height - 2
|
|
60
|
-
inner_width = width - 2
|
|
61
|
-
stdscr.addstr(1, 1, f"Current directory: {os.getcwd()}")
|
|
62
|
-
|
|
63
|
-
draw_hr(stdscr, 2)
|
|
64
|
-
|
|
65
|
-
# Header
|
|
66
|
-
max_alias_len = max(
|
|
67
|
-
max(len(entry["alias"]) for entry in filtered_DIRS.values()), 5
|
|
68
|
-
)
|
|
69
|
-
max_path_len = max(
|
|
70
|
-
max(len(entry["path"]) for entry in filtered_DIRS.values()), 4
|
|
71
|
-
)
|
|
72
|
-
max_id_len = max(max(len(alias_id) for alias_id in filtered_DIRS.keys()), 2)
|
|
73
|
-
|
|
74
|
-
alias_col = max_alias_len + 2
|
|
75
|
-
id_col = max_id_len + 2
|
|
76
|
-
path_col = max_path_len
|
|
77
|
-
|
|
78
|
-
header_text = f"{'ALIAS'.ljust(alias_col)}{'ID'.ljust(id_col)}{'PATH'.ljust(path_col)} CREATED AT"
|
|
79
|
-
stdscr.addstr(3, 1, header_text[:inner_width])
|
|
80
|
-
|
|
81
|
-
draw_hr(stdscr, 4)
|
|
82
|
-
|
|
83
|
-
# List entries
|
|
84
|
-
line_start = 5
|
|
85
|
-
for entry_id, entry in enumerate(filtered_DIRS.values()):
|
|
86
|
-
if line_start >= inner_height - 5:
|
|
87
|
-
break
|
|
88
|
-
alias = entry["alias"].ljust(max_alias_len)
|
|
89
|
-
path = entry["path"].ljust(max_path_len)
|
|
90
|
-
alias_id = list(filtered_DIRS.keys())[entry_id].ljust(max_id_len)
|
|
91
|
-
created_at = time.strftime(
|
|
92
|
-
"%Y-%m-%d %H:%M:%S", time.localtime(entry["created_at"])
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
line_text = f"{alias} {alias_id} {path} {created_at}"
|
|
96
|
-
if entry_id == selected_entry:
|
|
97
|
-
stdscr.addstr(line_start, 1, line_text[:inner_width], curses.A_REVERSE)
|
|
98
|
-
pre_selected_path = entry["path"]
|
|
99
|
-
else:
|
|
100
|
-
stdscr.addstr(line_start, 1, line_text[:inner_width])
|
|
101
|
-
|
|
102
|
-
line_start += 1
|
|
103
|
-
|
|
104
|
-
# Controls
|
|
105
|
-
controls_y = height - 5
|
|
106
|
-
draw_hr(stdscr, controls_y, curses.A_DIM)
|
|
107
|
-
controls_text = (
|
|
108
|
-
"ctrls: enter=select"
|
|
109
|
-
if search_mode
|
|
110
|
-
else "ctrls: ↑/k=up ↓/j=down enter=select d/backspace=delete q=exit search s=search"
|
|
111
|
-
if post_search_mode
|
|
112
|
-
else "ctrls: ↑/k=up ↓/j=down enter=select d/backspace=delete q=quit s=search"
|
|
113
|
-
)
|
|
114
|
-
stdscr.addstr(controls_y + 1, 1, controls_text, curses.A_DIM)
|
|
115
|
-
|
|
116
|
-
# Action area
|
|
117
|
-
action_area_y = height - 3
|
|
118
|
-
draw_hr(stdscr, action_area_y)
|
|
119
|
-
|
|
120
|
-
if search_mode:
|
|
121
|
-
stdscr.addstr(action_area_y + 1, 1, f"Search: {search_query}")
|
|
122
|
-
elif confirm_mode and action == "delete":
|
|
123
|
-
entry = filtered_DIRS[list(filtered_DIRS.keys())[selected_entry]]
|
|
124
|
-
stdscr.addstr(
|
|
125
|
-
action_area_y + 1,
|
|
126
|
-
1,
|
|
127
|
-
f"Delete entry '{entry['alias']}' ({entry['path']})? [enter/q]",
|
|
128
|
-
)
|
|
129
|
-
elif pre_selected_path:
|
|
130
|
-
stdscr.addstr(
|
|
131
|
-
action_area_y + 1,
|
|
132
|
-
1,
|
|
133
|
-
f"Command: cd {os.path.abspath(os.path.expanduser(pre_selected_path))}",
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
stdscr.refresh()
|
|
137
|
-
|
|
138
|
-
# Handle key events
|
|
139
|
-
key = stdscr.getch()
|
|
140
|
-
|
|
141
|
-
if search_mode:
|
|
142
|
-
if key == ord("\n"):
|
|
143
|
-
search_mode = False
|
|
144
|
-
post_search_mode = True
|
|
145
|
-
elif key == curses.KEY_BACKSPACE or key == 127:
|
|
146
|
-
search_query = search_query[:-1]
|
|
147
|
-
filter_dirs_by_search(search_query)
|
|
148
|
-
else:
|
|
149
|
-
search_query += chr(key)
|
|
150
|
-
filter_dirs_by_search(search_query)
|
|
151
|
-
elif post_search_mode:
|
|
152
|
-
if key == ord("q") or key == 27: # 'q' or 'esc'
|
|
153
|
-
filtered_DIRS = original_DIRS
|
|
154
|
-
post_search_mode = False
|
|
155
|
-
elif key == curses.KEY_UP or key == ord("k"):
|
|
156
|
-
selected_entry = max(0, selected_entry - 1)
|
|
157
|
-
elif key == curses.KEY_DOWN or key == ord("j"):
|
|
158
|
-
selected_entry = min(max_items - 1, selected_entry + 1)
|
|
159
|
-
elif key == ord("\n"):
|
|
160
|
-
selected_entry_id = list(filtered_DIRS.keys())[selected_entry]
|
|
161
|
-
return filtered_DIRS[selected_entry_id]
|
|
162
|
-
elif confirm_mode:
|
|
163
|
-
if key == ord("\n") and action == "delete":
|
|
164
|
-
selected_entry_id = list(filtered_DIRS.keys())[selected_entry]
|
|
165
|
-
data = crud.load_data(CONFIG)
|
|
166
|
-
try:
|
|
167
|
-
crud.delete_entry(CONFIG, data, selected_entry_id)
|
|
168
|
-
except KeyError:
|
|
169
|
-
error_log.error(f"Entry ID {selected_entry_id} not found")
|
|
170
|
-
del filtered_DIRS[selected_entry_id]
|
|
171
|
-
if selected_entry >= len(filtered_DIRS):
|
|
172
|
-
selected_entry = max(len(filtered_DIRS) - 1, 0)
|
|
173
|
-
confirm_mode = False
|
|
174
|
-
else:
|
|
175
|
-
confirm_mode = False
|
|
176
|
-
else:
|
|
177
|
-
if key == curses.KEY_UP or key == ord("k"):
|
|
178
|
-
selected_entry = (selected_entry - 1) % max_items
|
|
179
|
-
elif key == curses.KEY_DOWN or key == ord("j"):
|
|
180
|
-
selected_entry = (selected_entry + 1) % max_items
|
|
181
|
-
elif key == ord("\n"):
|
|
182
|
-
selected_entry_id = list(filtered_DIRS.keys())[selected_entry]
|
|
183
|
-
return filtered_DIRS[selected_entry_id]
|
|
184
|
-
elif key == ord("q"):
|
|
185
|
-
return None
|
|
186
|
-
elif key == ord("d") or key == curses.KEY_BACKSPACE:
|
|
187
|
-
confirm_mode = True
|
|
188
|
-
action = "delete"
|
|
189
|
-
elif key == ord("s"):
|
|
190
|
-
search_mode = True
|
|
191
|
-
selected_entry = 0
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def display_select(config, dirs):
|
|
195
|
-
global CONFIG, DIRS, filtered_DIRS, search_query, original_DIRS
|
|
196
|
-
CONFIG = config
|
|
197
|
-
DIRS = dirs
|
|
198
|
-
filtered_DIRS = DIRS
|
|
199
|
-
original_DIRS = DIRS
|
|
200
|
-
search_query = ""
|
|
201
|
-
return curses.wrapper(display_select_screen)
|