Skip to content

Instantly share code, notes, and snippets.

@theScottyJam
Last active September 27, 2021 05:13
Show Gist options
  • Save theScottyJam/ae19299c8d71a21c75f2abd96daea9f8 to your computer and use it in GitHub Desktop.
Save theScottyJam/ae19299c8d71a21c75f2abd96daea9f8 to your computer and use it in GitHub Desktop.
This adds a "new file" menu item to the nautilus right-click menu for when you right click on a file. Place this file inside `~/.local/share/nautilus/scripts` and name it `new`. This script is useful for when you are using nautilus in list view, because nautilus currently doesn't show this menu item when right clicking on files.
#!/usr/bin/env python3
# https://superuser.com/a/1251013/57284
# https://gist.github.com/theScottyJam/ae19299c8d71a21c75f2abd96daea9f8
from pathlib import Path
import tkinter as tk
from tkinter import ttk
import os
import shutil
TEMPLATES_DIR = Path('~/Templates').expanduser()
target_dir = None
def main():
global target_dir
# The following gets the selected item(s) from nautilus.
selected = os.environ.get('NAUTILUS_SCRIPT_SELECTED_FILE_PATHS', os.curdir)
# We're only going to use the first if multiple are selected.
target_dir = Path( selected.split('\n')[0] ).resolve().parent
templates = [f.name for f in TEMPLATES_DIR.iterdir()]
templates.sort()
if templates:
prompt = FilenamePrompt(templates, mk_file)
prompt.show()
else:
window = tk.Tk()
window.title('Error')
label = tk.Label(window, text="Error: You don't have any templates inside ~/Templates")
label.grid(row=0)
label.pack(padx=10, pady=10)
window.protocol("WM_DELETE_WINDOW", window.quit)
window.mainloop()
window.destroy()
def mk_file(template, filename):
""" Create a new file using by coping the chosen template to `current directory/filename` """
src_file = TEMPLATES_DIR/template
dst_file = target_dir/filename
if filename == '':
return 'Please provide a file name'
if not dst_file.parent.is_dir(): # If the inputed text has a "/" with an non-existant folder before it.
return 'That folder does not exist'
if dst_file.is_file():
return 'That file already exists'
try:
shutil.copy2(src_file, dst_file)
os.utime(dst_file)
except Exception as e:
return str(e)
class FilenamePrompt():
def __init__(self, templates, on_create_file):
"""
templates is a list of strings that will appear in the menu.
on_create_file is what gets called when the user pushes ok.
It gets passes the selected template, and the file name the user chose.
If an error occures, that error should return as a string that will get shown to the user.
Otherwise, None should be returned.
"""
self.templates = templates
self.on_create_file = on_create_file
self.filename_has_changed = False # Sets to true as soon as the user alters the content of the textbox.
# This can be set to true if this class wants to alter the textbox value without triggering other consequences.
self.ingore_textbox_change = False
self.error_is_shown = False
self.selected_template = None
def show(self):
""" Creates and displays the file naming dialog """
self.window = tk.Tk()
self.window.style = ttk.Style()
self.window.style.theme_use('default')
self.window.title('New Document')
self.window.resizable(False, False)
self.render(self.window)
self.window.mainloop()
self.window.destroy()
def render(self, window):
""" Renders the window and attaches event listeners """
self.render_body(window)
self.render_buttons(window)
# Tab order
self.listbox.lower()
self.textbox.lower()
self.ok_button.lower()
self.cancel_button.lower()
# Event listeners
window.bind('<Return>', self.apply_values)
window.bind('<Escape>', self.close_window)
window.bind('<Down>', self.select_one_down)
window.bind('<Up>', self.select_one_up)
window.protocol("WM_DELETE_WINDOW", self.close_window)
def render_body(self, window):
""" Renders the main body of the window, which includes the template choosing box
and the filename textbox """
body = ttk.Frame(window)
# listbox
self.listbox = tk.Listbox(window, selectmode=tk.SINGLE, width=40, selectbackground='#8af', exportselection=False)
self.listbox.bind('<<ListboxSelect>>', self.on_select_template)
self.listbox.pack()
for template in self.templates:
self.listbox.insert(tk.END, template)
# The following auto-resizes the listbox based on the content inside.
self.listbox.config(height=0)
# textbox
ttk.Label(body, text="File name: ").grid(row=0)
self.textbox_var = tk.StringVar()
self.textbox_var.trace('w', self.on_textbox_change)
self.textbox = ttk.Entry(body, textvariable=self.textbox_var)
self.textbox.grid(row=0, column=1)
body.pack(padx=5, pady=5)
# error label
err_label_frame = ttk.Frame(window)
self.err_label = tk.Label(err_label_frame, fg='#a00')
self.err_label.grid(row=0)
err_label_frame.pack()
# autoselects the first listbox item
self.listbox.select_set(0)
self.on_select_template()
def render_buttons(self, window):
""" Renders the buttons at the bottom of the window """
button_bar = ttk.Frame(window)
self.cancel_button = ttk.Button(button_bar, text="Cancel", width=10, command=self.close_window)
self.cancel_button.pack(side=tk.LEFT, padx=5, pady=5)
self.ok_button = ttk.Button(button_bar, text="OK", width=10, command=self.apply_values, default=tk.ACTIVE)
self.ok_button.pack(side=tk.LEFT, padx=5, pady=5)
button_bar.pack()
def set_error(self, text):
""" Displays some text to the user """
self.err_label.config(text=text)
self.textbox.selection_range(0, tk.END)
self.error_is_shown = True
self.textbox.focus_set()
# Event listeners
def on_textbox_change(self, *args):
""" Called when the user changes the file name """
if not self.ingore_textbox_change == True:
self.filename_has_changed = True
self.err_label.config(text='')
if self.textbox.get() == '':
# We'll allow the textbox content to change if it's currently empty.
self.filename_has_changed = False
def on_select_template(self, event=None):
""" Called when the user selects a different template """
self.selected_template = self.templates[ int(self.listbox.curselection()[0]) ]
if not self.filename_has_changed:
self.ingore_textbox_change = True
self.textbox_var.set(self.selected_template)
self.ingore_textbox_change = False
self.textbox.selection_range(0, tk.END)
self.window.after_idle(self.textbox.focus)
def select_one_down(self, event):
self.move_selection(1)
def select_one_up(self, event):
self.move_selection(-1)
def move_selection(self, amount):
""" Changes the template selection. """
cur_selection = self.listbox.curselection()[0]
new_selection = int(cur_selection) + amount
if new_selection < 0:
new_selection = 0
if new_selection >= len(self.templates):
new_selection = len(self.templates) - 1
self.listbox.selection_clear(cur_selection)
self.listbox.select_set(new_selection)
self.on_select_template()
def close_window(self, event=None):
self.window.quit()
def apply_values(self, event=None):
""" Usually called with OK is pushed """
message = self.on_create_file(self.selected_template, self.textbox.get())
if message:
self.set_error(message)
return
self.window.quit()
if __name__ == '__main__':
main()
@theScottyJam
Copy link
Author

Thanks @gene1wood for the contribution - I've gone ahead and applied your changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment