Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- import os
- import re
- import curses
- import subprocess
- import json
- import argparse
- import signal
- import shutil
- from datetime import datetime, timedelta
- def get_config_file():
- """Get the path to the configuration file."""
- script_name = os.path.splitext(os.path.basename(__file__))[0]
- config_file = f"{script_name}_conf.txt"
- return os.path.join(os.path.dirname(__file__), config_file)
- def load_config():
- """Load the entire configuration from a plain text file."""
- config_file = get_config_file()
- default_config = {
- "SEARCH_DIR": os.path.expanduser("~/Documents/text_files/"),
- "TEMP_COPY_DIR": os.path.expanduser("~/Documents/text_files/temp_copy"),
- "favorites": [],
- "editor": "nano"
- }
- config = default_config.copy()
- if os.path.exists(config_file):
- with open(config_file, 'r') as f:
- lines = f.readlines()
- current_favorite = {}
- for line in lines:
- line = line.strip()
- if line.startswith("favorite_") and "=" in line:
- key, value = line.split('=', 1)
- if key.endswith("_filename"):
- if current_favorite:
- config["favorites"].append(current_favorite)
- current_favorite = {"filename": value}
- elif key.endswith("_position"):
- current_favorite["position"] = int(value)
- elif key.endswith("_search_text"):
- current_favorite["search_text"] = value
- else:
- key, value = line.split('=', 1)
- config[key] = value
- if current_favorite:
- config["favorites"].append(current_favorite)
- return config
- def save_default_config():
- """Save the default configuration to a plain text file if it doesn't exist."""
- config_file = get_config_file()
- if not os.path.exists(config_file):
- default_config = {
- "SEARCH_DIR": os.path.expanduser("~/Documents/text_files/"),
- "TEMP_COPY_DIR": os.path.expanduser("~/Documents/text_files/temp_copy"),
- "editor": "nano"
- }
- save_config(default_config)
- def save_config(config):
- """Save the entire configuration to a plain text file."""
- config_file = get_config_file()
- with open(config_file, 'w') as f:
- f.write(f"SEARCH_DIR={config['SEARCH_DIR']}\n")
- f.write(f"TEMP_COPY_DIR={config['TEMP_COPY_DIR']}\n")
- f.write(f"editor={config.get('editor', 'nano')}\n")
- if 'last_file' in config:
- f.write(f"last_file={config['last_file']}\n")
- if 'position' in config:
- f.write(f"position={config['position']}\n")
- if 'search_text' in config:
- f.write(f"search_text={config['search_text']}\n")
- for i, fav in enumerate(config.get("favorites", []), 1):
- f.write(f"favorite_{i}_filename={fav['filename']}\n")
- f.write(f"favorite_{i}_position={fav['position']}\n")
- f.write(f"favorite_{i}_search_text={fav['search_text']}\n")
- def save_position(filename, position, search_text=""):
- """Save the last position of the file being read."""
- data = load_config()
- data["last_file"] = filename
- data["position"] = position
- data["search_text"] = search_text
- save_config(data)
- def load_position():
- """Load the last position of the file read."""
- data = load_config()
- return {
- "last_file": data.get("last_file"),
- "position": int(data.get("position", 0)),
- "search_text": data.get("search_text", "")
- }
- def add_to_favorites(filename, position, search_text):
- """Add the current file to the favorites list."""
- config = load_config()
- favorites = config.get("favorites", [])
- favorites.append({
- "filename": filename,
- "position": position,
- "search_text": search_text
- })
- config["favorites"] = favorites
- save_config(config)
- def list_files(directory):
- """List all text files in the given directory and its subdirectories."""
- file_list = []
- for root, _, files in os.walk(directory):
- for file in files:
- if file.endswith(".txt"):
- file_list.append(os.path.join(root, file))
- return file_list
- def read_file(file_path):
- """Read a file with different encodings to avoid UnicodeDecodeError."""
- encodings = ['utf-8', 'latin-1', 'iso-8859-1']
- for encoding in encodings:
- try:
- with open(file_path, 'r', encoding=encoding) as file:
- return file.readlines()
- except UnicodeDecodeError:
- continue
- return []
- def save_search_results(search_term, results):
- """Save search results to a cache file."""
- config = load_config()
- cache_dir = os.path.join(config["TEMP_COPY_DIR"], "cache")
- if not os.path.exists(cache_dir):
- os.makedirs(cache_dir)
- cache_file = os.path.join(cache_dir, f"{search_term}.json")
- data = {
- "timestamp": datetime.now().isoformat(),
- "results": results
- }
- with open(cache_file, 'w') as f:
- json.dump(data, f)
- def load_search_results(search_term):
- """Load search results from a cache file if they are recent."""
- config = load_config()
- cache_dir = os.path.join(config["TEMP_COPY_DIR"], "cache")
- cache_file = os.path.join(cache_dir, f"{search_term}.json")
- if os.path.exists(cache_file):
- with open(cache_file, 'r') as f:
- data = json.load(f)
- timestamp = datetime.fromisoformat(data["timestamp"])
- if datetime.now() - timestamp < timedelta(days=1):
- return data["results"]
- return None
- def search_files(file_list, search_pattern):
- """Search for a term within a list of files and count matches, using cached results if available."""
- cached_results = load_search_results(search_pattern)
- if cached_results is not None:
- return cached_results
- results = []
- pattern = re.compile(search_pattern, re.IGNORECASE)
- for file_path in file_list:
- lines = read_file(file_path)
- match_count = sum(1 for line in lines if pattern.search(line))
- if match_count > 0:
- results.append((file_path, match_count))
- save_search_results(search_pattern, results)
- return results
- def copy_file_to_temp_copy(file_path):
- """Copy the given file to the temp_copy directory."""
- config = load_config()
- shutil.copy(file_path, config["TEMP_COPY_DIR"])
- def open_in_editor(file_path, line_number):
- """Open the file in the configured editor at the specified line number."""
- config = load_config()
- editor = config.get("editor", "nano")
- # End the curses session before opening the editor
- curses.endwin()
- if editor == "nano":
- subprocess.call([editor, f"+{line_number}", file_path])
- elif editor == "xdg-open":
- subprocess.call([editor, file_path])
- else:
- raise ValueError("Unsupported editor configured.")
- # Reinitialize the curses session after closing the editor
- stdscr = curses.initscr()
- curses.start_color()
- curses.use_default_colors()
- curses.cbreak()
- stdscr.keypad(True)
- curses.noecho()
- return stdscr
- def display_file(stdscr, file_path, start_pos=0, search_text=""):
- """Display the file using curses and pandoc."""
- def find_matches(lines, pattern):
- """Find all matches of the pattern in the lines."""
- matches = []
- regex = re.compile(pattern, re.IGNORECASE)
- for i, line in enumerate(lines):
- if regex.search(line):
- matches.append(i)
- return matches
- def display_status(stdscr, file_path, file_size_kb, percentage_read, search_mode, search_text="", match_idx=0, total_matches=0):
- """Display the status bar."""
- max_y, max_x = stdscr.getmaxyx()
- status = f"File: {os.path.basename(file_path)} | Size: {file_size_kb:.2f} KB | {percentage_read:.2f}% read"
- stdscr.addstr(0, 0, status[:max_x-1], curses.A_REVERSE)
- if search_mode:
- search_status = f"Search: {search_text} ({match_idx}/{total_matches})"
- stdscr.addstr(max_y-1, 0, search_status[:max_x-1], curses.A_REVERSE)
- else:
- stdscr.addstr(max_y-1, 0, " " * (max_x-1), curses.A_REVERSE)
- def handle_resize(signum, frame):
- """Handle window resize signal."""
- curses.resizeterm(*stdscr.getmaxyx())
- signal.signal(signal.SIGWINCH, handle_resize)
- # Check for bookmark, favorite, or last read position
- config = load_config()
- last_position = load_position()
- # Check if the file is the last read file
- if last_position['last_file'] == file_path:
- start_pos = last_position['position']
- search_text = last_position.get('search_text', '')
- # Check if the file is in the favorites
- bookmarks = [fav for fav in config.get('favorites', []) if fav['filename'] == file_path]
- if bookmarks:
- start_pos = bookmarks[0]['position']
- search_text = bookmarks[0].get('search_text', '')
- while True:
- try:
- with open(file_path, 'rb') as f:
- content = f.read().decode('utf-8', errors='ignore')
- except Exception as e:
- return
- # Use pandoc to format the text
- process = subprocess.Popen(['pandoc', '-f', 'markdown', '-t', 'plain'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
- formatted_text, _ = process.communicate(input=content.encode())
- lines = formatted_text.decode().split('\n')
- file_size_kb = os.path.getsize(file_path) / 1024
- max_y, max_x = stdscr.getmaxyx()
- pos = start_pos
- search_mode = bool(search_text)
- matches = find_matches(lines, search_text) if search_mode else []
- current_match = 0
- while True:
- stdscr.clear()
- # Status bar
- percentage_read = (pos / len(lines)) * 100 if lines else 0
- display_status(stdscr, file_path, file_size_kb, percentage_read, search_mode, search_text, current_match + 1, len(matches))
- for i, line in enumerate(lines[pos:pos+max_y-2]):
- line_display = line[:max_x-1]
- if search_mode and search_text.lower() in line.lower():
- start_idx = line.lower().find(search_text.lower())
- stdscr.addstr(i+1, 0, line_display[:start_idx])
- stdscr.addstr(i+1, start_idx, line_display[start_idx:start_idx+len(search_text)], curses.A_REVERSE)
- stdscr.addstr(i+1, start_idx+len(search_text), line_display[start_idx+len(search_text):])
- else:
- stdscr.addstr(i+1, 0, line_display)
- stdscr.refresh()
- key = stdscr.getch()
- if key == curses.KEY_DOWN and pos < len(lines) - max_y + 2:
- pos += 1
- if search_mode:
- # Update current match index based on position
- while current_match < len(matches) and matches[current_match] < pos:
- current_match += 1
- elif key == curses.KEY_UP and pos > 0:
- pos -= 1
- if search_mode:
- # Update current match index based on position
- while current_match > 0 and matches[current_match - 1] >= pos:
- current_match -= 1
- elif key == curses.KEY_NPAGE: # Page Down
- pos = min(pos + max_y - 2, len(lines) - max_y + 2)
- if search_mode:
- # Update current match index based on position
- while current_match < len(matches) and matches[current_match] < pos:
- current_match += 1
- elif key == curses.KEY_PPAGE: # Page Up
- pos = max(pos - (max_y - 2), 0)
- if search_mode:
- # Update current match index based on position
- while current_match > 0 and matches[current_match - 1] >= pos:
- current_match -= 1
- elif key == ord('s'):
- search_mode = True
- search_text = ""
- stdscr.addstr(max_y-1, 0, "Search: ", curses.A_REVERSE)
- curses.echo()
- search_text = stdscr.getstr(max_y-1, 8).decode('utf-8')
- curses.noecho()
- matches = find_matches(lines, search_text)
- current_match = 0
- if matches:
- pos = max(matches[current_match] - 2, 0)
- elif key == ord('x'):
- search_mode = False
- search_text = ""
- elif search_mode and key == curses.KEY_RIGHT and matches:
- if current_match < len(matches) - 1:
- current_match += 1
- pos = max(matches[current_match] - 2, 0)
- elif search_mode and key == curses.KEY_LEFT and matches:
- if current_match > 0:
- current_match -= 1
- pos = max(matches[current_match] - 2, 0)
- elif key in map(ord, '0123456789'):
- percent = int(chr(key)) * 10
- pos = min(int((percent / 100) * len(lines)), len(lines) - max_y + 2)
- if search_mode:
- # Update current match index based on position
- while current_match < len(matches) and matches[current_match] < pos:
- current_match += 1
- elif key == ord('c'):
- copy_file_to_temp_copy(file_path)
- elif key == ord('f'):
- add_to_favorites(file_path, pos, search_text)
- elif key == ord('e'):
- stdscr = open_in_editor(file_path, pos + 1)
- break # Exit to reload file after editing
- elif key == ord('q'):
- save_position(file_path, pos, search_text)
- return
- # Reload the file content after editing
- curses.curs_set(1)
- stdscr.clear()
- stdscr.refresh()
- curses.curs_set(0)
- def display_favorites(stdscr):
- """Display the list of favorite files."""
- config = load_config()
- favorites = config.get("favorites", [])
- if not favorites:
- stdscr.addstr(0, 0, "No favorites saved.", curses.color_pair(1))
- stdscr.refresh()
- stdscr.getch()
- return
- current_row = 0
- top_row = 0
- max_file_size_len = 10 # Set a constant width for the file size column
- while True:
- stdscr.clear()
- max_y, max_x = stdscr.getmaxyx()
- stdscr.addstr(0, 0, "Favorites")
- for idx, fav in enumerate(favorites[top_row:top_row + max_y - 2]):
- row_idx = idx + 1
- if row_idx >= max_y:
- break
- if idx + top_row == current_row:
- stdscr.attron(curses.A_REVERSE)
- file_size_kb = os.path.getsize(fav['filename']) / 1024
- file_size_display = f"{file_size_kb:.2f} KB".ljust(max_file_size_len)
- display_text = f"{file_size_display} | {os.path.basename(fav['filename'])} (Position: {fav['position']}, Search: {fav['search_text']})"
- if len(display_text) > max_x - 1:
- display_text = display_text[:max_x - 4] + '...'
- stdscr.addstr(row_idx, 0, display_text)
- if idx + top_row == current_row:
- stdscr.attroff(curses.A_REVERSE)
- stdscr.refresh()
- key = stdscr.getch()
- if key == curses.KEY_DOWN and current_row < len(favorites) - 1:
- current_row += 1
- if current_row >= top_row + max_y - 2:
- top_row += 1
- elif key == curses.KEY_UP and current_row > 0:
- current_row -= 1
- if current_row < top_row:
- top_row -= 1
- elif key == curses.KEY_RIGHT or key == ord('\n'):
- if favorites:
- fav = favorites[current_row]
- display_file(stdscr, fav['filename'], fav['position'], fav['search_text'])
- elif key == ord('q'):
- break
- elif key == curses.KEY_BACKSPACE:
- if favorites:
- del favorites[current_row]
- config["favorites"] = favorites
- save_config(config)
- if current_row >= len(favorites):
- current_row = len(favorites) - 1
- if top_row > current_row:
- top_row = current_row
- def search_mode(stdscr, search_text=""):
- """Search mode to handle user input and search results."""
- curses.curs_set(0)
- current_row = 0
- top_row = 0
- results = []
- error_message = ""
- sort_mode = 0 # 0: Name, 1: Size Ascending, 2: Size Descending, 3: Matches Ascending, 4: Matches Descending
- config = load_config()
- files = list_files(config["SEARCH_DIR"])
- results = list(search_files(files, search_text))
- if not results:
- error_message = "No matches found."
- else:
- error_message = ""
- base_path = config["SEARCH_DIR"]
- max_file_size_len = 10 # Set a constant width for the file size column
- while True:
- if sort_mode == 1:
- results.sort(key=lambda x: os.path.getsize(x[0]))
- elif sort_mode == 2:
- results.sort(key=lambda x: os.path.getsize(x[0]), reverse=True)
- elif sort_mode == 3:
- results.sort(key=lambda x: (x[1], os.path.getsize(x[0])), reverse=False)
- elif sort_mode == 4:
- results.sort(key=lambda x: (x[1], os.path.getsize(x[0])), reverse=True)
- else:
- results.sort(key=lambda x: x[0])
- stdscr.clear()
- sort_text = ["Name", "Size Ascending", "Size Descending", "Matches Ascending", "Matches Descending"][sort_mode]
- status_text = f"Search: {search_text} | Sort by: {sort_text} | Result {current_row + 1}/{len(results)}"
- stdscr.addstr(0, 0, status_text[:stdscr.getmaxyx()[1]-1])
- if error_message:
- stdscr.addstr(1, 0, f"Error: {error_message}", curses.color_pair(1))
- if results:
- max_y, max_x = stdscr.getmaxyx()
- max_display_rows = max_y - 3
- for idx, (result, count) in enumerate(results[top_row:top_row + max_display_rows]):
- row_idx = idx + 2
- if row_idx >= max_y:
- break
- if idx + top_row == current_row:
- stdscr.attron(curses.A_REVERSE)
- display_text = result.replace(base_path, "", 1) # Remove the base path only once
- file_size_kb = os.path.getsize(result) / 1024
- count_display = f"{count:03}".replace(" ", ".")
- file_size_display = f"{file_size_kb:.2f} KB".ljust(max_file_size_len)
- display_text = f"{count_display} | {file_size_display} | {display_text}"
- if len(display_text) > max_x - 1:
- display_text = display_text[:max_x - 4] + '...'
- stdscr.addstr(row_idx, 0, display_text)
- if idx + top_row == current_row:
- stdscr.attroff(curses.A_REVERSE)
- stdscr.refresh()
- key = stdscr.getch()
- if key == curses.KEY_DOWN and results and current_row < len(results) - 1:
- current_row += 1
- if current_row >= top_row + max_display_rows - 4:
- top_row = min(top_row + 1, len(results) - max_display_rows)
- elif key == curses.KEY_UP and current_row > 0:
- current_row -= 1
- if current_row < top_row + 4:
- top_row = max(top_row - 1, 0)
- elif key == curses.KEY_RIGHT or key == ord('\n'):
- if results:
- display_file(stdscr, results[current_row][0], search_text=search_text)
- elif key == ord('c'):
- copy_file_to_temp_copy(results[current_row][0])
- elif key == ord('t'):
- sort_mode = (sort_mode + 1) % 5 # Update to cycle through 5 sorting modes
- elif key == ord('f'):
- display_favorites(stdscr)
- elif key == ord('e'):
- if results:
- stdscr = open_in_editor(results[current_row][0], 1) # Open at the first line
- # Reload the search results after editing
- results = list(search_files(files, search_text))
- if sort_mode == 1:
- results.sort(key=lambda x: os.path.getsize(x[0]))
- elif sort_mode == 2:
- results.sort(key=lambda x: os.path.getsize(x[0]), reverse=True)
- elif sort_mode == 3:
- results.sort(key=lambda x: (x[1], os.path.getsize(x[0])), reverse=False)
- elif sort_mode == 4:
- results.sort(key=lambda x: (x[1], os.path.getsize(x[0])), reverse=True)
- else:
- results.sort(key=lambda x: x[0])
- elif key == ord('q'):
- break
- def resume_mode(stdscr):
- """Resume mode to open the last viewed file at the last position."""
- config = load_position()
- last_file = config['last_file']
- last_pos = config['position']
- if last_file:
- display_file(stdscr, last_file, last_pos)
- if __name__ == '__main__':
- save_default_config() # Ensure default config is saved if it doesn't exist
- parser = argparse.ArgumentParser(description="Search and view text files.")
- parser.add_argument('-l', '--last', action='store_true', help="Open the last viewed file at the last position.")
- parser.add_argument('-f', '--favorites', action='store_true', help="Display the list of favorite files.")
- parser.add_argument('search_text', nargs='?', type=str, help="The search term.")
- args = parser.parse_args()
- if args.last:
- curses.wrapper(resume_mode)
- elif args.favorites:
- curses.wrapper(display_favorites)
- elif args.search_text:
- curses.wrapper(search_mode, args.search_text)
- else:
- last_position = load_position()
- if last_position["last_file"]:
- curses.wrapper(display_file, last_position["last_file"], last_position["position"], last_position["search_text"])
- else:
- print("Usage: smutreader [-l] [-f] [search_text]")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement