twd-m4sc0 1.5.4__py3-none-any.whl → 2.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/crud.py +104 -0
- twd/logger.py +41 -41
- twd/screen.py +201 -0
- twd/twd.py +124 -140
- {twd_m4sc0-1.5.4.dist-info → twd_m4sc0-2.0.1.dist-info}/METADATA +4 -1
- twd_m4sc0-2.0.1.dist-info/RECORD +14 -0
- {twd_m4sc0-1.5.4.dist-info → twd_m4sc0-2.0.1.dist-info}/WHEEL +1 -1
- twd_m4sc0-1.5.4.dist-info/RECORD +0 -12
- {twd_m4sc0-1.5.4.dist-info → twd_m4sc0-2.0.1.dist-info}/LICENSE +0 -0
- {twd_m4sc0-1.5.4.dist-info → twd_m4sc0-2.0.1.dist-info}/entry_points.txt +0 -0
- {twd_m4sc0-1.5.4.dist-info → twd_m4sc0-2.0.1.dist-info}/top_level.txt +0 -0
twd/crud.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import os
|
|
2
|
-
import
|
|
3
|
+
from logging.handlers import RotatingFileHandler
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
def
|
|
6
|
-
"""
|
|
7
|
-
result = config.get("log_format", "[$T]: $M")
|
|
8
|
-
result = result.replace("$T", time.strftime("%Y-%m-%d %H:%M:%S"))
|
|
9
|
-
result = result.replace("$M", message)
|
|
10
|
-
return result
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def ensure_directory_exists(file_path):
|
|
14
|
-
"""Ensure the directory for the given file path exists."""
|
|
15
|
-
directory = os.path.dirname(file_path)
|
|
16
|
-
if not os.path.exists(directory):
|
|
17
|
-
os.makedirs(directory, exist_ok=True)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def write_log(message, config):
|
|
21
|
-
"""Write log message to the log file specified in the config."""
|
|
6
|
+
def setup_logger(config):
|
|
7
|
+
"""Set up loggers based on the configuration provided."""
|
|
22
8
|
log_file = os.path.expanduser(config.get("log_file"))
|
|
23
|
-
log_file = os.path.abspath(log_file)
|
|
24
|
-
ensure_directory_exists(log_file) # Ensure the directory exists
|
|
25
|
-
with open(log_file, "a+") as f:
|
|
26
|
-
f.write(message + "\n")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def write_error(message, config):
|
|
30
|
-
"""Write error message to the error file specified in the config."""
|
|
31
9
|
error_file = os.path.expanduser(config.get("error_file"))
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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)
|
|
43
47
|
|
|
44
|
-
def error(message, config):
|
|
45
|
-
"""Log the error using the provided config."""
|
|
46
|
-
formatted_message = format_message(message, config)
|
|
47
|
-
write_error(formatted_message, config)
|
twd/screen.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
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)
|
twd/twd.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import argparse
|
|
3
3
|
import json
|
|
4
|
-
import hashlib
|
|
5
4
|
import time
|
|
6
5
|
import re
|
|
6
|
+
import logging
|
|
7
7
|
from importlib.metadata import version, PackageNotFoundError
|
|
8
|
-
from .logger import
|
|
8
|
+
from .logger import initialize_logging
|
|
9
|
+
from .screen import display_select
|
|
10
|
+
from . import crud
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger("log")
|
|
13
|
+
error_log = logging.getLogger("error")
|
|
9
14
|
|
|
10
15
|
TWD_DIR = os.path.join(os.path.expanduser("~"), ".twd")
|
|
11
16
|
CONFIG_FILE = os.path.join(TWD_DIR, "config")
|
|
@@ -15,19 +20,16 @@ DEFAULT_CONFIG = {
|
|
|
15
20
|
"output_behaviour": 2,
|
|
16
21
|
"log_file": os.path.expanduser("~/.twd/log"),
|
|
17
22
|
"error_file": os.path.expanduser("~/.twd/error"),
|
|
18
|
-
"log_format": "
|
|
23
|
+
"log_format": "%(asctime)s - %(levelname)s - %(message)s",
|
|
24
|
+
"log_level": "INFO",
|
|
25
|
+
"log_max_bytes": 5 * 1024 * 1024, # 5 MB log rotation
|
|
26
|
+
"log_backup_count": 3,
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
# os.makedirs(TWD_DIR, exist_ok=True)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def create_alias_id():
|
|
25
|
-
data = str(time.time()) + str(os.urandom(16))
|
|
26
|
-
return hashlib.sha256(data.encode()).hexdigest()[:12]
|
|
27
|
-
|
|
28
29
|
|
|
29
30
|
def load_config():
|
|
30
31
|
if not os.path.exists(CONFIG_FILE):
|
|
32
|
+
os.makedirs(TWD_DIR, exist_ok=True)
|
|
31
33
|
with open(CONFIG_FILE, "w") as file:
|
|
32
34
|
json.dump(DEFAULT_CONFIG, file, indent=4)
|
|
33
35
|
return DEFAULT_CONFIG
|
|
@@ -36,23 +38,18 @@ def load_config():
|
|
|
36
38
|
try:
|
|
37
39
|
return json.load(file)
|
|
38
40
|
except json.JSONDecodeError as e:
|
|
39
|
-
error(f"Error loading config: {e}"
|
|
41
|
+
error_log.error(f"Error loading config: {e}")
|
|
40
42
|
return DEFAULT_CONFIG
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
CONFIG = load_config()
|
|
46
|
+
initialize_logging(CONFIG)
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
crud.ensure_data_file_exists(CONFIG)
|
|
47
49
|
|
|
48
|
-
def ensure_data_file_exists():
|
|
49
|
-
if not os.path.exists(TWD_FILE):
|
|
50
|
-
try:
|
|
51
|
-
with open(TWD_FILE, "w") as f:
|
|
52
|
-
json.dump({}, f)
|
|
53
|
-
except OSError as e:
|
|
54
|
-
error(f"Error creating data file: {e}", CONFIG)
|
|
55
50
|
|
|
51
|
+
def ensure_log_error_files():
|
|
52
|
+
"""Ensure that log and error files exist based on configuration settings."""
|
|
56
53
|
log_file = os.path.expanduser(CONFIG.get("log_file"))
|
|
57
54
|
error_file = os.path.expanduser(CONFIG.get("error_file"))
|
|
58
55
|
|
|
@@ -61,31 +58,31 @@ def ensure_data_file_exists():
|
|
|
61
58
|
with open(log_file, "w+") as f:
|
|
62
59
|
f.write("")
|
|
63
60
|
except OSError as e:
|
|
64
|
-
error(f"Error creating log file: {e}"
|
|
61
|
+
error_log.error(f"Error creating log file: {e}")
|
|
65
62
|
|
|
66
63
|
if not os.path.exists(error_file):
|
|
67
64
|
try:
|
|
68
65
|
with open(error_file, "w+") as f:
|
|
69
66
|
f.write("")
|
|
70
67
|
except OSError as e:
|
|
71
|
-
error(f"Error creating error file: {e}"
|
|
68
|
+
error_log.error(f"Error creating error file: {e}")
|
|
72
69
|
|
|
73
70
|
|
|
74
|
-
|
|
71
|
+
ensure_log_error_files()
|
|
75
72
|
|
|
76
73
|
|
|
77
74
|
def get_absolute_path(path):
|
|
78
75
|
try:
|
|
79
76
|
return os.path.abspath(path)
|
|
80
77
|
except Exception as e:
|
|
81
|
-
error(f"Error getting absolute path for {path}: {e}"
|
|
78
|
+
error_log.error(f"Error getting absolute path for {path}: {e}")
|
|
82
79
|
raise
|
|
83
80
|
|
|
84
81
|
|
|
85
82
|
def validate_alias(alias):
|
|
86
83
|
"""Ensure the alias contains only valid characters."""
|
|
87
84
|
if not re.match(r"^[\w-]+$", alias):
|
|
88
|
-
error(f"Invalid alias provided: {alias}"
|
|
85
|
+
error_log.error(f"Invalid alias provided: {alias}")
|
|
89
86
|
raise ValueError(
|
|
90
87
|
f"Invalid alias: '{alias}'. Aliases can only contain alphanumeric characters, dashes, and underscores."
|
|
91
88
|
)
|
|
@@ -95,21 +92,20 @@ def validate_alias(alias):
|
|
|
95
92
|
def output_handler(
|
|
96
93
|
message=None, path=None, output=True, simple_output=False, message_type=0
|
|
97
94
|
):
|
|
98
|
-
log(f"Type: {message_type}, Msg: {message or path}"
|
|
99
|
-
|
|
100
|
-
if
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
print(f"0;{message}")
|
|
95
|
+
log.info(f"Type: {message_type}, Msg: {message or path}")
|
|
96
|
+
|
|
97
|
+
if CONFIG["output_behaviour"] == 1 or simple_output:
|
|
98
|
+
if path:
|
|
99
|
+
with open("/tmp/twd_path", "w") as f:
|
|
100
|
+
f.write(path)
|
|
101
|
+
if output:
|
|
102
|
+
print(path)
|
|
103
|
+
elif CONFIG["output_behaviour"] == 2:
|
|
104
|
+
if path:
|
|
105
|
+
with open("/tmp/twd_path", "w") as f:
|
|
106
|
+
f.write(path)
|
|
107
|
+
if output:
|
|
108
|
+
print(message)
|
|
113
109
|
|
|
114
110
|
|
|
115
111
|
def save_directory(path=None, alias=None, output=True, simple_output=False):
|
|
@@ -121,26 +117,8 @@ def save_directory(path=None, alias=None, output=True, simple_output=False):
|
|
|
121
117
|
if alias:
|
|
122
118
|
alias = validate_alias(alias)
|
|
123
119
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
data = json.load(f)
|
|
127
|
-
except json.JSONDecodeError as e:
|
|
128
|
-
error(f"Error reading TWD file: {e}", CONFIG)
|
|
129
|
-
data = {}
|
|
130
|
-
|
|
131
|
-
alias_id = create_alias_id()
|
|
132
|
-
data[alias_id] = {
|
|
133
|
-
"path": path,
|
|
134
|
-
"alias": alias if alias else alias_id,
|
|
135
|
-
"created_at": time.time(),
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try:
|
|
139
|
-
with open(TWD_FILE, "w") as f:
|
|
140
|
-
json.dump(data, f, indent=4)
|
|
141
|
-
except OSError as e:
|
|
142
|
-
error(f"Error writing to TWD file: {e}", CONFIG)
|
|
143
|
-
raise
|
|
120
|
+
data = crud.load_data(CONFIG)
|
|
121
|
+
alias_id = crud.create_entry(CONFIG, data, path, alias)
|
|
144
122
|
|
|
145
123
|
output_handler(
|
|
146
124
|
f"Saved TWD to {path} with alias '{alias or alias_id}'",
|
|
@@ -151,47 +129,79 @@ def save_directory(path=None, alias=None, output=True, simple_output=False):
|
|
|
151
129
|
|
|
152
130
|
|
|
153
131
|
def load_directory():
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
try:
|
|
158
|
-
with open(TWD_FILE, "r") as f:
|
|
159
|
-
return json.load(f)
|
|
160
|
-
except json.JSONDecodeError as e:
|
|
161
|
-
error(f"Error loading TWD file: {e}", CONFIG)
|
|
162
|
-
return None
|
|
132
|
+
data = crud.load_data(CONFIG)
|
|
133
|
+
return data if data else None
|
|
163
134
|
|
|
164
135
|
|
|
165
|
-
def
|
|
136
|
+
def show_main(alias=None, output=True, simple_output=False):
|
|
166
137
|
dirs = load_directory()
|
|
167
|
-
|
|
168
|
-
if not dirs:
|
|
138
|
+
if dirs is None:
|
|
169
139
|
output_handler("No TWD found", None, output, simple_output)
|
|
170
140
|
return 1
|
|
171
141
|
else:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
142
|
+
if alias:
|
|
143
|
+
matched_dirs = []
|
|
144
|
+
|
|
145
|
+
for entry_id, entry in dirs.items():
|
|
146
|
+
if (
|
|
147
|
+
"alias" in entry
|
|
148
|
+
and entry["alias"]
|
|
149
|
+
and entry["alias"].startswith(alias)
|
|
150
|
+
):
|
|
151
|
+
entry["id"] = entry_id
|
|
152
|
+
matched_dirs.append(entry)
|
|
153
|
+
elif entry_id.startswith(alias):
|
|
154
|
+
entry["id"] = entry_id
|
|
155
|
+
matched_dirs.append(entry)
|
|
156
|
+
|
|
157
|
+
if len(matched_dirs) == 1:
|
|
158
|
+
TWD = matched_dirs[0]["path"]
|
|
176
159
|
if os.path.exists(TWD):
|
|
177
160
|
output_handler(
|
|
178
161
|
f"cd {TWD}", TWD, output, simple_output, message_type=1
|
|
179
162
|
)
|
|
180
163
|
return 0
|
|
181
164
|
else:
|
|
182
|
-
error(f"Directory does not exist: {TWD}"
|
|
165
|
+
error_log.error(f"Directory does not exist: {TWD}")
|
|
183
166
|
output_handler(
|
|
184
167
|
f"Directory does not exist: {TWD}", None, output, simple_output
|
|
185
168
|
)
|
|
186
169
|
return 1
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
170
|
+
elif len(matched_dirs) > 1:
|
|
171
|
+
output_handler(
|
|
172
|
+
f"Multiple TWDs match for '{alias}':", None, output, simple_output
|
|
173
|
+
)
|
|
174
|
+
for match in matched_dirs:
|
|
175
|
+
output_handler(
|
|
176
|
+
f"{match['alias']} {match['id']} {match['path']}",
|
|
177
|
+
None,
|
|
178
|
+
output,
|
|
179
|
+
simple_output,
|
|
180
|
+
)
|
|
181
|
+
return 1
|
|
182
|
+
else:
|
|
183
|
+
output_handler("No TWD with alias found", None, output, simple_output)
|
|
184
|
+
return 1
|
|
185
|
+
|
|
186
|
+
selected_dir = display_select(CONFIG, dirs)
|
|
187
|
+
if selected_dir is None:
|
|
188
|
+
output_handler("No TWD selected", None, output, simple_output)
|
|
189
|
+
return 0
|
|
190
|
+
else:
|
|
191
|
+
TWD = selected_dir["path"]
|
|
192
|
+
if os.path.exists(TWD):
|
|
193
|
+
output_handler(f"cd {TWD}", TWD, output, simple_output, message_type=1)
|
|
194
|
+
return 0
|
|
195
|
+
else:
|
|
196
|
+
error_log.error(f"Directory does not exist: {TWD}")
|
|
197
|
+
output_handler(
|
|
198
|
+
f"Directory does not exist: {TWD}", None, output, simple_output
|
|
199
|
+
)
|
|
200
|
+
return 1
|
|
190
201
|
|
|
191
202
|
|
|
192
203
|
def show_directory(output=True, simple_output=False):
|
|
193
204
|
dirs = load_directory()
|
|
194
|
-
|
|
195
205
|
if not dirs:
|
|
196
206
|
output_handler("No TWD set", None, output, simple_output)
|
|
197
207
|
return
|
|
@@ -220,50 +230,41 @@ def show_directory(output=True, simple_output=False):
|
|
|
220
230
|
|
|
221
231
|
|
|
222
232
|
def unset_directory(output=True, simple_output=False, force=False):
|
|
223
|
-
if not
|
|
224
|
-
output_handler(
|
|
225
|
-
|
|
226
|
-
if not force:
|
|
227
|
-
output_handler(
|
|
228
|
-
r"""If you want to execute deleting and therefore unsetting all set TWD's, please use "--force" or "-f" and run again.
|
|
229
|
-
|
|
233
|
+
if not force:
|
|
234
|
+
output_handler(
|
|
235
|
+
r"""If you want to execute deleting and therefore unsetting all set TWD's, please use "--force" or "-f" and run again.
|
|
230
236
|
|
|
231
237
|
This feature is to prevent accidental execution.""",
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
None,
|
|
239
|
+
True,
|
|
240
|
+
False,
|
|
241
|
+
)
|
|
242
|
+
return
|
|
243
|
+
try:
|
|
244
|
+
crud.delete_data_file(CONFIG)
|
|
245
|
+
except OSError as e:
|
|
246
|
+
error_log.error(f"Error deleting TWD file: {e}")
|
|
247
|
+
raise
|
|
248
|
+
output_handler("TWD File deleted and TWD unset", None, output, simple_output)
|
|
243
249
|
|
|
244
250
|
|
|
245
251
|
def get_package_version():
|
|
246
252
|
try:
|
|
247
253
|
return version("twd_m4sc0")
|
|
248
254
|
except PackageNotFoundError as e:
|
|
249
|
-
error(f"Package version not found: {e}"
|
|
255
|
+
error_log.error(f"Package version not found: {e}")
|
|
250
256
|
return "Unknown version"
|
|
251
257
|
|
|
252
258
|
|
|
253
259
|
def main():
|
|
254
|
-
global TWD_FILE
|
|
255
|
-
|
|
256
260
|
parser = argparse.ArgumentParser(
|
|
257
261
|
description="Temporarily save and navigate to working directories."
|
|
258
262
|
)
|
|
259
263
|
|
|
260
|
-
# Positional arguments
|
|
261
264
|
parser.add_argument("directory", nargs="?", help="Directory to save")
|
|
262
265
|
parser.add_argument(
|
|
263
266
|
"alias", nargs="?", help="Alias for the saved directory (optional)"
|
|
264
267
|
)
|
|
265
|
-
|
|
266
|
-
# Optional Arguments/Flags
|
|
267
268
|
parser.add_argument(
|
|
268
269
|
"-s",
|
|
269
270
|
"--save",
|
|
@@ -273,7 +274,7 @@ def main():
|
|
|
273
274
|
parser.add_argument("-d", "--dir", nargs="?", help="Directory to save")
|
|
274
275
|
parser.add_argument("-a", "--ali", nargs="?", help="Alias for the saved directory")
|
|
275
276
|
parser.add_argument(
|
|
276
|
-
"-g", "--go", nargs="?", const=
|
|
277
|
+
"-g", "--go", nargs="?", const=" ", help="Go to the saved directory"
|
|
277
278
|
)
|
|
278
279
|
parser.add_argument("-l", "--list", action="store_true", help="Show saved TWD")
|
|
279
280
|
parser.add_argument(
|
|
@@ -283,66 +284,49 @@ def main():
|
|
|
283
284
|
"-v",
|
|
284
285
|
"--version",
|
|
285
286
|
action="version",
|
|
286
|
-
version=f"TWD Version: {get_package_version()}",
|
|
287
|
-
help="Show the current version of TWD installed",
|
|
287
|
+
version=f"TWD Version: v{get_package_version()}",
|
|
288
288
|
)
|
|
289
289
|
parser.add_argument("-f", "--force", action="store_true", help="Force an action")
|
|
290
290
|
parser.add_argument(
|
|
291
291
|
"--shell", nargs="?", const="twd", help="Output shell function for integration"
|
|
292
292
|
)
|
|
293
293
|
parser.add_argument(
|
|
294
|
-
"--simple-output",
|
|
295
|
-
action="store_true",
|
|
296
|
-
help="Only print essential output (new directory, absolute path, etc.)",
|
|
294
|
+
"--simple-output", action="store_true", help="Only print essential output"
|
|
297
295
|
)
|
|
298
296
|
parser.add_argument(
|
|
299
297
|
"--no-output",
|
|
300
298
|
action="store_true",
|
|
301
299
|
help="Prevents the console from sending output",
|
|
302
300
|
)
|
|
303
|
-
args = parser.parse_args()
|
|
304
301
|
|
|
302
|
+
args = parser.parse_args()
|
|
305
303
|
output = not args.no_output
|
|
306
304
|
simple_output = args.simple_output
|
|
307
305
|
|
|
308
306
|
if args.shell:
|
|
309
|
-
print(rf"""
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
type=$(echo "$line" | cut -d';' -f1);
|
|
317
|
-
message=$(echo "$line" | cut -d';' -f2-);
|
|
318
|
-
if [[ "$type" == "1" ]]; then
|
|
319
|
-
eval "$message";
|
|
320
|
-
else
|
|
321
|
-
echo "$message";
|
|
322
|
-
fi;
|
|
323
|
-
done <<< "$output";
|
|
324
|
-
}}
|
|
325
|
-
""")
|
|
307
|
+
print(rf"""function {args.shell}() {{
|
|
308
|
+
python3 -m twd "$@";
|
|
309
|
+
if [ -f /tmp/twd_path ]; then
|
|
310
|
+
cd "$(cat /tmp/twd_path)";
|
|
311
|
+
/bin/rm -f /tmp/twd_path;
|
|
312
|
+
fi;
|
|
313
|
+
}}""")
|
|
326
314
|
return 0
|
|
327
315
|
|
|
328
316
|
directory = args.directory or args.dir
|
|
329
317
|
alias = args.alias or args.ali
|
|
330
318
|
|
|
331
319
|
if args.save:
|
|
332
|
-
if not directory:
|
|
333
|
-
directory = args.directory or os.getcwd()
|
|
334
|
-
|
|
335
|
-
alias = args.alias or args.ali
|
|
336
|
-
|
|
337
320
|
save_directory(directory, alias, output, simple_output)
|
|
338
321
|
elif args.go:
|
|
339
|
-
alias
|
|
340
|
-
return go_to_directory(alias, output, simple_output)
|
|
322
|
+
show_main(alias, output, simple_output)
|
|
341
323
|
elif args.list:
|
|
342
324
|
show_directory(output, simple_output)
|
|
343
325
|
elif args.unset:
|
|
344
|
-
force
|
|
345
|
-
unset_directory(output, simple_output, force)
|
|
326
|
+
unset_directory(output, simple_output, force=args.force)
|
|
346
327
|
else:
|
|
347
|
-
|
|
348
|
-
|
|
328
|
+
show_main(None, output, simple_output)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
main()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: twd_m4sc0
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: A tool to temporarily save and go to a working directory
|
|
5
5
|
Home-page: https://github.com/m4sc0/twd
|
|
6
6
|
Author: m4sc0
|
|
@@ -16,6 +16,8 @@ License-File: LICENSE
|
|
|
16
16
|
|
|
17
17
|
`twd-m4sc0` is a command-line tool that allows you to temporarily save a working directory and easily navigate back to it. It's designed for developers and users who frequently need to switch between directories in the terminal.
|
|
18
18
|
|
|
19
|
+
> All Versions `< v1.5` are considered deprecated and should not be used anymore because of the `config` file that was introduced in that version. This file is incompatible with newer versions and might cause issues or break the program.
|
|
20
|
+
|
|
19
21
|
## Features
|
|
20
22
|
|
|
21
23
|
- Save the current or specified working directory.
|
|
@@ -23,6 +25,7 @@ License-File: LICENSE
|
|
|
23
25
|
- List all saved directories with metadata.
|
|
24
26
|
- Unset and delete saved directories.
|
|
25
27
|
- Integrates with your shell for seamless directory management.
|
|
28
|
+
- Some options can be configured using the `config` file. For more information please visit the [Config](CONFIG.md) documentation.
|
|
26
29
|
|
|
27
30
|
## Installation
|
|
28
31
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
tests/test_twd.py,sha256=XrxOo99oqida_7qZ48pA6BCazZekDmG5syw9NvjZWDU,484
|
|
3
|
+
twd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
twd/__main__.py,sha256=gM_Py51pQFH3CXdDIuPzBW6eNlFH1UHeRAmgAPWQyik,61
|
|
5
|
+
twd/crud.py,sha256=QTruLgK9aBorJ1VKpeBS-joeD1DZbK2JfQm9tl3v5Bc,3057
|
|
6
|
+
twd/logger.py,sha256=WG-JBbkdizR2KMODGcxREj1N--CgY-PdIC_agyZ5PHU,1590
|
|
7
|
+
twd/screen.py,sha256=vQFNtast5-P_XrdvbY7XyIx2MGoNR7TUoVHHkXUTbvc,7066
|
|
8
|
+
twd/twd.py,sha256=m-wad_GY6plzaatBKatkd5vp4UTV_fjXdHJauzInPYQ,10488
|
|
9
|
+
twd_m4sc0-2.0.1.dist-info/LICENSE,sha256=eQSDjcD_fvOwfjmrzxKJhtZsSI39seMawuvsgeD_dfw,1062
|
|
10
|
+
twd_m4sc0-2.0.1.dist-info/METADATA,sha256=BhP5_39Pb76v64cvxgDrsbauBCB4MYO5tfQdigh5LJg,3528
|
|
11
|
+
twd_m4sc0-2.0.1.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
|
|
12
|
+
twd_m4sc0-2.0.1.dist-info/entry_points.txt,sha256=QfYDHHjipkVN4oalpACFmIeYHb7GQCJY4SC12bTsiQQ,37
|
|
13
|
+
twd_m4sc0-2.0.1.dist-info/top_level.txt,sha256=PXToru2Yr2Xh3F_F-pHXtuOQVp5x7KKCPFf94P_VI5U,10
|
|
14
|
+
twd_m4sc0-2.0.1.dist-info/RECORD,,
|
twd_m4sc0-1.5.4.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
tests/test_twd.py,sha256=XrxOo99oqida_7qZ48pA6BCazZekDmG5syw9NvjZWDU,484
|
|
3
|
-
twd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
twd/__main__.py,sha256=gM_Py51pQFH3CXdDIuPzBW6eNlFH1UHeRAmgAPWQyik,61
|
|
5
|
-
twd/logger.py,sha256=Pmhh4sOB-HyJAVBSwiESGRJElJoJ4Anhn-HVF8vxXzY,1595
|
|
6
|
-
twd/twd.py,sha256=EUsAOweKNl3zV2QnITkEjdG6Nv6UQjaUu3w7RaR__nQ,10344
|
|
7
|
-
twd_m4sc0-1.5.4.dist-info/LICENSE,sha256=eQSDjcD_fvOwfjmrzxKJhtZsSI39seMawuvsgeD_dfw,1062
|
|
8
|
-
twd_m4sc0-1.5.4.dist-info/METADATA,sha256=Mg_V_ltMMGp2hBInKxtZf8bokIpNJUDS1wBKJgR_F7s,3157
|
|
9
|
-
twd_m4sc0-1.5.4.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
|
10
|
-
twd_m4sc0-1.5.4.dist-info/entry_points.txt,sha256=QfYDHHjipkVN4oalpACFmIeYHb7GQCJY4SC12bTsiQQ,37
|
|
11
|
-
twd_m4sc0-1.5.4.dist-info/top_level.txt,sha256=PXToru2Yr2Xh3F_F-pHXtuOQVp5x7KKCPFf94P_VI5U,10
|
|
12
|
-
twd_m4sc0-1.5.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|