代码片段管理工具:rofi前端+Java守护进程
This commit is contained in:
40
CodeSnippetRofi/common/constant.py
Normal file
40
CodeSnippetRofi/common/constant.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import os
|
||||
|
||||
code_snippet_dir = os.getenv("CODE_SNIPPET_DIR")
|
||||
code_snippet_port = os.getenv("CODE_SNIPPET_PORT")
|
||||
code_snippet_rofi = os.getenv("CODE_SNIPPET_ROFI")
|
||||
|
||||
action_list = "LIST"
|
||||
action_add = "ADD"
|
||||
action_edit = "EDIT"
|
||||
action_delete = "DELETE"
|
||||
|
||||
template_add = """
|
||||
## Snippet
|
||||
|
||||
```[Language]
|
||||
|
||||
```
|
||||
|
||||
## MetaData
|
||||
|
||||
- Name
|
||||
-
|
||||
- Language
|
||||
-
|
||||
"""
|
||||
template_edit = """
|
||||
## Snippet
|
||||
|
||||
```[Language]
|
||||
|
||||
```
|
||||
|
||||
## MetaData
|
||||
|
||||
- Tags
|
||||
-
|
||||
-
|
||||
- Description
|
||||
-
|
||||
"""
|
||||
823
CodeSnippetRofi/common/rofi.py
Normal file
823
CodeSnippetRofi/common/rofi.py
Normal file
@@ -0,0 +1,823 @@
|
||||
#
|
||||
# python-rofi
|
||||
#
|
||||
# The MIT License
|
||||
#
|
||||
# Copyright (c) 2016, 2017 Blair Bonnett <blair.bonnett@gmail.com>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
|
||||
import atexit
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, InvalidOperation
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# Python < 3.2 doesn't provide a context manager interface for Popen.
|
||||
# Let's make our own wrapper if needed.
|
||||
if hasattr(subprocess.Popen, '__exit__'):
|
||||
Popen = subprocess.Popen
|
||||
else:
|
||||
class ContextManagedPopen(subprocess.Popen):
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if self.stdout:
|
||||
self.stdout.close()
|
||||
if self.stderr:
|
||||
self.stderr.close()
|
||||
if self.stdin:
|
||||
self.stdin.close()
|
||||
self.wait()
|
||||
|
||||
|
||||
Popen = ContextManagedPopen
|
||||
|
||||
|
||||
class Rofi(object):
|
||||
"""Class to facilitate making simple GUIs with Rofi.
|
||||
|
||||
Rofi is a popup window system with minimal dependencies (xlib and pango).
|
||||
It was designed as a window switcher. Its basic operation is to display a
|
||||
list of options and let the user pick one.
|
||||
|
||||
This class provides a set of methods to make simple GUIs with Rofi. It does
|
||||
this by using the subprocess module to call Rofi externally. Many of the
|
||||
methods are blocking.
|
||||
|
||||
Some strings can contain Pango markup for additional formatting (those that
|
||||
can are noted as such in the docstrings). Any text in these strings *must*
|
||||
be escaped before calling Rofi. The class method Rofi.escape() performs
|
||||
this escaping for you. Make sure you call this on the text prior to adding
|
||||
Pango markup, otherwise the markup will be escaped and displayed to the
|
||||
user. See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html
|
||||
for available markup.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, lines=None, fixed_lines=None, width=None,
|
||||
fullscreen=None, location=None,
|
||||
exit_hotkeys=('Alt+F4', 'Control+q'), rofi_cmd=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
exit_hotkeys: tuple of strings
|
||||
Hotkeys to use to exit the application. These will be automatically
|
||||
set and handled in any method which takes hotkey arguments. If one
|
||||
of these hotkeys is pressed, a SystemExit will be raised to perform
|
||||
the exit.
|
||||
|
||||
The following parameters set default values for various layout options,
|
||||
and can be overwritten in any display method. A value of None means
|
||||
use the system default, which may be set by a configuration file or
|
||||
fall back to the compile-time default. See the Rofi documentation for
|
||||
full details on what the values mean.
|
||||
|
||||
lines: positive integer
|
||||
The maximum number of lines to show before scrolling.
|
||||
fixed_lines: positive integer
|
||||
Keep a fixed number of lines visible.
|
||||
width: real
|
||||
If positive but not more than 100, this is the percentage of the
|
||||
screen's width the window takes up. If greater than 100, it is the
|
||||
width in pixels. If negative, it estimates the width required for
|
||||
the corresponding number of characters, i.e., -30 would set the
|
||||
width so ~30 characters per row would show.
|
||||
fullscreen: boolean
|
||||
If True, use the full height and width of the screen.
|
||||
location: integer
|
||||
The position of the window on the screen.
|
||||
|
||||
"""
|
||||
# The Popen class returned for any non-blocking windows.
|
||||
self._process = None
|
||||
|
||||
# Save parameters.
|
||||
self.lines = lines
|
||||
self.fixed_lines = fixed_lines
|
||||
self.width = width
|
||||
self.fullscreen = fullscreen
|
||||
self.location = location
|
||||
self.exit_hotkeys = exit_hotkeys
|
||||
|
||||
if rofi_cmd is None:
|
||||
self.rofi = "rofi"
|
||||
else:
|
||||
self.rofi = rofi_cmd
|
||||
# Don't want a window left on the screen if we exit unexpectedly
|
||||
# (e.g., an unhandled exception).
|
||||
atexit.register(self.close)
|
||||
|
||||
@classmethod
|
||||
def escape(self, string):
|
||||
"""Escape a string for Pango markup.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
string:
|
||||
A piece of text to escape.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The text, safe for use in with Pango markup.
|
||||
|
||||
"""
|
||||
# Escape ampersands first, then other entities. Since argument is a
|
||||
# dictionary, we can't guarantee order of translations and so doing it
|
||||
# in one go would risk the ampersands in other translations being
|
||||
# escaped again.
|
||||
return string.translate(
|
||||
{38: '&'}
|
||||
).translate({
|
||||
34: '"',
|
||||
39: ''',
|
||||
60: '<',
|
||||
62: '>'
|
||||
})
|
||||
|
||||
def close(self):
|
||||
"""Close any open window.
|
||||
|
||||
Note that this only works with non-blocking methods.
|
||||
|
||||
"""
|
||||
if self._process:
|
||||
# Be nice first.
|
||||
self._process.send_signal(signal.SIGINT)
|
||||
|
||||
# If it doesn't close itself promptly, be brutal.
|
||||
# Python 3.2+ added the timeout option to wait() and the
|
||||
# corresponding TimeoutExpired exception. If they exist, use them.
|
||||
if hasattr(subprocess, 'TimeoutExpired'):
|
||||
try:
|
||||
self._process.wait(timeout=1)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._process.send_signal(signal.SIGKILL)
|
||||
|
||||
# Otherwise, roll our own polling loop.
|
||||
else:
|
||||
# Give it 1s, checking every 10ms.
|
||||
count = 0
|
||||
while count < 100:
|
||||
if self._process.poll() is not None:
|
||||
break
|
||||
time.sleep(0.01)
|
||||
|
||||
# Still hasn't quit.
|
||||
if self._process.poll() is None:
|
||||
self._process.send_signal(signal.SIGKILL)
|
||||
|
||||
# Clean up.
|
||||
self._process = None
|
||||
|
||||
def _run_blocking(self, args, input=None):
|
||||
"""Internal API: run a blocking command with subprocess.
|
||||
|
||||
This closes any open non-blocking dialog before running the command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
args: Popen constructor arguments
|
||||
Command to run.
|
||||
input: string
|
||||
Value to feed to the stdin of the process.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(returncode, stdout)
|
||||
The exit code (integer) and stdout value (string) from the process.
|
||||
|
||||
"""
|
||||
# Close any existing dialog.
|
||||
if self._process:
|
||||
self.close()
|
||||
|
||||
# Make sure we grab stdout as text (not bytes).
|
||||
kwargs = {}
|
||||
kwargs['stdout'] = subprocess.PIPE
|
||||
kwargs['universal_newlines'] = True
|
||||
|
||||
# Use the run() method if available (Python 3.5+).
|
||||
if hasattr(subprocess, 'run'):
|
||||
result = subprocess.run(args, input=input, **kwargs)
|
||||
return result.returncode, result.stdout
|
||||
|
||||
# Have to do our own. If we need to feed stdin, we must open a pipe.
|
||||
if input is not None:
|
||||
kwargs['stdin'] = subprocess.PIPE
|
||||
|
||||
# Start the process.
|
||||
with Popen(args, **kwargs) as proc:
|
||||
# Talk to it (no timeout). This will wait until termination.
|
||||
stdout, stderr = proc.communicate(input)
|
||||
|
||||
# Find out the return code.
|
||||
returncode = proc.poll()
|
||||
|
||||
# Done.
|
||||
return returncode, stdout
|
||||
|
||||
def _run_nonblocking(self, args, input=None):
|
||||
"""Internal API: run a non-blocking command with subprocess.
|
||||
|
||||
This closes any open non-blocking dialog before running the command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
args: Popen constructor arguments
|
||||
Command to run.
|
||||
input: string
|
||||
Value to feed to the stdin of the process.
|
||||
|
||||
"""
|
||||
# Close any existing dialog.
|
||||
if self._process:
|
||||
self.close()
|
||||
|
||||
# Start the new one.
|
||||
self._process = subprocess.Popen(args, stdout=subprocess.PIPE)
|
||||
|
||||
def _common_args(self, allow_fullscreen=True, **kwargs):
|
||||
args = []
|
||||
|
||||
# Number of lines.
|
||||
lines = kwargs.get('lines', self.lines)
|
||||
if lines:
|
||||
args.extend(['-lines', str(lines)])
|
||||
fixed_lines = kwargs.get('fixed_lines', self.fixed_lines)
|
||||
if fixed_lines:
|
||||
args.extend(['-fixed-num-lines', str(fixed_lines)])
|
||||
|
||||
# Width.
|
||||
width = kwargs.get('width', self.width)
|
||||
if width is not None:
|
||||
args.extend(['-width', str(width)])
|
||||
|
||||
# Fullscreen mode?
|
||||
fullscreen = kwargs.get('fullscreen', self.fullscreen)
|
||||
if allow_fullscreen and fullscreen:
|
||||
args.append('-fullscreen')
|
||||
|
||||
# Location on screen.
|
||||
location = kwargs.get('location', self.location)
|
||||
if location is not None:
|
||||
args.extend(['-location', str(location)])
|
||||
|
||||
# Done.
|
||||
return args
|
||||
|
||||
def error(self, message, **kwargs):
|
||||
"""Show an error window.
|
||||
|
||||
This method blocks until the user presses a key.
|
||||
|
||||
Fullscreen mode is not supported for error windows, and if specified
|
||||
will be ignored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message: string
|
||||
Error message to show.
|
||||
|
||||
"""
|
||||
# Generate arguments list.
|
||||
args = [self.rofi, '-e', message]
|
||||
args.extend(self._common_args(allow_fullscreen=False, **kwargs))
|
||||
|
||||
# Close any existing window and show the error.
|
||||
self._run_blocking(args)
|
||||
|
||||
def status(self, message, **kwargs):
|
||||
"""Show a status message.
|
||||
|
||||
This method is non-blocking, and intended to give a status update to
|
||||
the user while something is happening in the background.
|
||||
|
||||
To close the window, either call the close() method or use any of the
|
||||
display methods to replace it with a different window.
|
||||
|
||||
Fullscreen mode is not supported for status messages and if specified
|
||||
will be ignored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
message: string
|
||||
Progress message to show.
|
||||
|
||||
"""
|
||||
# Generate arguments list.
|
||||
args = [self.rofi, '-e', message]
|
||||
args.extend(self._common_args(allow_fullscreen=False, **kwargs))
|
||||
|
||||
# Update the status.
|
||||
self._run_nonblocking(args)
|
||||
|
||||
def select(self, prompt, options, message="", select=None, **kwargs):
|
||||
"""Show a list of options and return user selection.
|
||||
|
||||
This method blocks until the user makes their choice.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prompt: string
|
||||
The prompt telling the user what they are selecting.
|
||||
options: list of strings
|
||||
The options they can choose from. Any newline characters are
|
||||
replaced with spaces.
|
||||
message: string, optional
|
||||
Message to show between the prompt and the options. This can
|
||||
contain Pango markup, and any text content should be escaped.
|
||||
select: integer, optional
|
||||
Set which option is initially selected.
|
||||
keyN: tuple (string, string); optional
|
||||
Custom key bindings where N is one or greater. The first entry in
|
||||
the tuple should be a string defining the key, e.g., "Alt+x" or
|
||||
"Delete". Note that letter keys should be lowercase ie.e., Alt+a
|
||||
not Alt+A.
|
||||
|
||||
The second entry should be a short string stating the action the
|
||||
key will take. This is displayed to the user at the top of the
|
||||
dialog. If None or an empty string, it is not displayed (but the
|
||||
binding is still set).
|
||||
|
||||
By default, key1 through key9 are set to ("Alt+1", None) through
|
||||
("Alt+9", None) respectively.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple (index, key)
|
||||
The index of the option the user selected, or -1 if they cancelled
|
||||
the dialog.
|
||||
Key indicates which key was pressed, with 0 being 'OK' (generally
|
||||
Enter), -1 being 'Cancel' (generally escape), and N being custom
|
||||
key N.
|
||||
|
||||
"""
|
||||
# Replace newlines and turn the options into a single string.
|
||||
optionstr = '\n'.join(option.replace('\n', ' ') for option in options)
|
||||
|
||||
# Set up arguments.
|
||||
args = [self.rofi, '-dmenu', '-p', prompt, '-format', 'i']
|
||||
if select is not None:
|
||||
args.extend(['-selected-row', str(select)])
|
||||
|
||||
# Key bindings to display.
|
||||
display_bindings = []
|
||||
|
||||
# Configure the key bindings.
|
||||
user_keys = set()
|
||||
for k, v in kwargs.items():
|
||||
# See if the keyword name matches the needed format.
|
||||
if not k.startswith('key'):
|
||||
continue
|
||||
try:
|
||||
keynum = int(k[3:])
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Add it to the set.
|
||||
key, action = v
|
||||
user_keys.add(keynum)
|
||||
args.extend(['-kb-custom-{0:s}'.format(k[3:]), key])
|
||||
if action:
|
||||
display_bindings.append("<b>{0:s}</b>: {1:s}".format(key, action))
|
||||
|
||||
# And the global exit bindings.
|
||||
exit_keys = set()
|
||||
next_key = 10
|
||||
for key in self.exit_hotkeys:
|
||||
while next_key in user_keys:
|
||||
next_key += 1
|
||||
exit_keys.add(next_key)
|
||||
args.extend(['-kb-custom-{0:d}'.format(next_key), key])
|
||||
next_key += 1
|
||||
|
||||
# Add any displayed key bindings to the message.
|
||||
message = message or ""
|
||||
if display_bindings:
|
||||
message += "\n" + " ".join(display_bindings)
|
||||
message = message.strip()
|
||||
|
||||
# If we have a message, add it to the arguments.
|
||||
if message:
|
||||
args.extend(['-mesg', message])
|
||||
|
||||
# Add in common arguments.
|
||||
args.extend(self._common_args(**kwargs))
|
||||
|
||||
# Run the dialog.
|
||||
returncode, stdout = self._run_blocking(args, input=optionstr)
|
||||
|
||||
# Figure out which option was selected.
|
||||
stdout = stdout.strip()
|
||||
index = int(stdout) if stdout else -1
|
||||
|
||||
# And map the return code to a key.
|
||||
if returncode == 0:
|
||||
key = 0
|
||||
elif returncode == 1:
|
||||
key = -1
|
||||
elif returncode > 9:
|
||||
key = returncode - 9
|
||||
if key in exit_keys:
|
||||
raise SystemExit()
|
||||
else:
|
||||
self.exit_with_error("Unexpected rofi returncode {0:d}.".format(results.returncode))
|
||||
|
||||
# And return.
|
||||
return index, key
|
||||
|
||||
def generic_entry(self, prompt, validator=None, message=None, **kwargs):
|
||||
"""A generic entry box.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prompt: string
|
||||
Text prompt for the entry.
|
||||
validator: function, optional
|
||||
A function to validate and convert the value entered by the user.
|
||||
It should take one parameter, the string that the user entered, and
|
||||
return a tuple (value, error). The value should be the users entry
|
||||
converted to the appropriate Python type, or None if the entry was
|
||||
invalid. The error message should be a string telling the user what
|
||||
was wrong, or None if the entry was valid. The prompt will be
|
||||
re-displayed to the user (along with the error message) until they
|
||||
enter a valid value. If no validator is given, the text that the
|
||||
user entered is returned as-is.
|
||||
message: string
|
||||
Optional message to display under the entry.
|
||||
|
||||
Returns
|
||||
-------
|
||||
The value returned by the validator, or None if the dialog was
|
||||
cancelled.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Enforce a minimum entry length:
|
||||
>>> r = Rofi()
|
||||
>>> validator = lambda s: (s, None) if len(s) > 6 else (None, "Too short")
|
||||
>>> r.generic_entry('Enter a 7-character or longer string: ', validator)
|
||||
|
||||
"""
|
||||
error = ""
|
||||
|
||||
# Keep going until we get something valid.
|
||||
while True:
|
||||
args = [self.rofi, '-dmenu', '-p', prompt, '-format', 's']
|
||||
|
||||
# Add any error to the given message.
|
||||
msg = message or ""
|
||||
if error:
|
||||
msg = '<span color="#FF0000" font_weight="bold">{0:s}</span>\n{1:s}'.format(error, msg)
|
||||
msg = msg.rstrip('\n')
|
||||
|
||||
# If there is actually a message to show.
|
||||
if msg:
|
||||
args.extend(['-mesg', msg])
|
||||
|
||||
# Add in common arguments.
|
||||
args.extend(self._common_args(**kwargs))
|
||||
|
||||
# Run it.
|
||||
returncode, stdout = self._run_blocking(args, input="")
|
||||
|
||||
# Was the dialog cancelled?
|
||||
if returncode == 1:
|
||||
return None
|
||||
|
||||
# Get rid of the trailing newline and check its validity.
|
||||
text = stdout.rstrip('\n')
|
||||
if validator:
|
||||
value, error = validator(text)
|
||||
if not error:
|
||||
return value
|
||||
else:
|
||||
return text
|
||||
|
||||
def text_entry(self, prompt, message=None, allow_blank=False, strip=True, **kwargs):
|
||||
"""Prompt the user to enter a piece of text.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prompt: string
|
||||
Prompt to display to the user.
|
||||
message: string, optional
|
||||
Message to display under the entry line.
|
||||
allow_blank: Boolean
|
||||
Whether to allow blank entries.
|
||||
strip: Boolean
|
||||
Whether to strip leading and trailing whitespace from the entered
|
||||
value.
|
||||
|
||||
Returns
|
||||
-------
|
||||
string, or None if the dialog was cancelled.
|
||||
|
||||
"""
|
||||
|
||||
def text_validator(text):
|
||||
if strip:
|
||||
text = text.strip()
|
||||
if not allow_blank:
|
||||
if not text:
|
||||
return None, "A value is required."
|
||||
|
||||
return text, None
|
||||
|
||||
return self.generic_entry(prompt, text_validator, message, **kwargs)
|
||||
|
||||
def integer_entry(self, prompt, message=None, min=None, max=None, **kwargs):
|
||||
"""Prompt the user to enter an integer.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prompt: string
|
||||
Prompt to display to the user.
|
||||
message: string, optional
|
||||
Message to display under the entry line.
|
||||
min, max: integer, optional
|
||||
Minimum and maximum values to allow. If None, no limit is imposed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
integer, or None if the dialog is cancelled.
|
||||
|
||||
"""
|
||||
# Sanity check.
|
||||
if (min is not None) and (max is not None) and not (max > min):
|
||||
raise ValueError("Maximum limit has to be more than the minimum limit.")
|
||||
|
||||
def integer_validator(text):
|
||||
error = None
|
||||
|
||||
# Attempt to convert to integer.
|
||||
try:
|
||||
value = int(text)
|
||||
except ValueError:
|
||||
return None, "Please enter an integer value."
|
||||
|
||||
# Check its within limits.
|
||||
if (min is not None) and (value < min):
|
||||
return None, "The minimum allowable value is {0:d}.".format(min)
|
||||
if (max is not None) and (value > max):
|
||||
return None, "The maximum allowable value is {0:d}.".format(max)
|
||||
|
||||
return value, None
|
||||
|
||||
return self.generic_entry(prompt, integer_validator, message, **kwargs)
|
||||
|
||||
def float_entry(self, prompt, message=None, min=None, max=None, **kwargs):
|
||||
"""Prompt the user to enter a floating point number.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prompt: string
|
||||
Prompt to display to the user.
|
||||
message: string, optional
|
||||
Message to display under the entry line.
|
||||
min, max: float, optional
|
||||
Minimum and maximum values to allow. If None, no limit is imposed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float, or None if the dialog is cancelled.
|
||||
|
||||
"""
|
||||
# Sanity check.
|
||||
if (min is not None) and (max is not None) and not (max > min):
|
||||
raise ValueError("Maximum limit has to be more than the minimum limit.")
|
||||
|
||||
def float_validator(text):
|
||||
error = None
|
||||
|
||||
# Attempt to convert to float.
|
||||
try:
|
||||
value = float(text)
|
||||
except ValueError:
|
||||
return None, "Please enter a floating point value."
|
||||
|
||||
# Check its within limits.
|
||||
if (min is not None) and (value < min):
|
||||
return None, "The minimum allowable value is {0}.".format(min)
|
||||
if (max is not None) and (value > max):
|
||||
return None, "The maximum allowable value is {0}.".format(max)
|
||||
|
||||
return value, None
|
||||
|
||||
return self.generic_entry(prompt, float_validator, message, **kwargs)
|
||||
|
||||
def decimal_entry(self, prompt, message=None, min=None, max=None, **kwargs):
|
||||
"""Prompt the user to enter a decimal number.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prompt: string
|
||||
Prompt to display to the user.
|
||||
message: string, optional
|
||||
Message to display under the entry line.
|
||||
min, max: Decimal, optional
|
||||
Minimum and maximum values to allow. If None, no limit is imposed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Decimal, or None if the dialog is cancelled.
|
||||
|
||||
"""
|
||||
# Sanity check.
|
||||
if (min is not None) and (max is not None) and not (max > min):
|
||||
raise ValueError("Maximum limit has to be more than the minimum limit.")
|
||||
|
||||
def decimal_validator(text):
|
||||
error = None
|
||||
|
||||
# Attempt to convert to decimal.
|
||||
try:
|
||||
value = Decimal(text)
|
||||
except InvalidOperation:
|
||||
return None, "Please enter a decimal value."
|
||||
|
||||
# Check its within limits.
|
||||
if (min is not None) and (value < min):
|
||||
return None, "The minimum allowable value is {0}.".format(min)
|
||||
if (max is not None) and (value > max):
|
||||
return None, "The maximum allowable value is {0}.".format(max)
|
||||
|
||||
return value, None
|
||||
|
||||
return self.generic_entry(prompt, decimal_validator, message, **kwargs)
|
||||
|
||||
def date_entry(self, prompt, message=None, formats=['%x', '%d/%m/%Y'], show_example=False, **kwargs):
|
||||
"""Prompt the user to enter a date.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prompt: string
|
||||
Prompt to display to the user.
|
||||
message: string, optional
|
||||
Message to display under the entry line.
|
||||
formats: list of strings, optional
|
||||
The formats that the user can enter dates in. These should be
|
||||
format strings as accepted by the datetime.datetime.strptime()
|
||||
function from the standard library. They are tried in order, and
|
||||
the first that returns a date object without error is selected.
|
||||
Note that the '%x' in the default list is the current locale's date
|
||||
representation.
|
||||
show_example: Boolean
|
||||
If True, today's date in the first format given is appended to the
|
||||
message.
|
||||
|
||||
Returns
|
||||
-------
|
||||
datetime.date, or None if the dialog is cancelled.
|
||||
|
||||
"""
|
||||
|
||||
def date_validator(text):
|
||||
# Try them in order.
|
||||
for format in formats:
|
||||
try:
|
||||
dt = datetime.strptime(text, format)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
# This one worked; good enough for us.
|
||||
return (dt.date(), None)
|
||||
|
||||
# None of the formats worked.
|
||||
return (None, 'Please enter a valid date.')
|
||||
|
||||
# Add an example to the message?
|
||||
if show_example:
|
||||
message = message or ""
|
||||
message += "Today's date in the correct format: " + datetime.now().strftime(formats[0])
|
||||
|
||||
return self.generic_entry(prompt, date_validator, message, **kwargs)
|
||||
|
||||
def time_entry(self, prompt, message=None, formats=['%X', '%H:%M', '%I:%M', '%H.%M', '%I.%M'], show_example=False,
|
||||
**kwargs):
|
||||
"""Prompt the user to enter a time.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prompt: string
|
||||
Prompt to display to the user.
|
||||
message: string, optional
|
||||
Message to display under the entry line.
|
||||
formats: list of strings, optional
|
||||
The formats that the user can enter times in. These should be
|
||||
format strings as accepted by the datetime.datetime.strptime()
|
||||
function from the standard library. They are tried in order, and
|
||||
the first that returns a time object without error is selected.
|
||||
Note that the '%X' in the default list is the current locale's time
|
||||
representation.
|
||||
show_example: Boolean
|
||||
If True, the current time in the first format given is appended to
|
||||
the message.
|
||||
|
||||
Returns
|
||||
-------
|
||||
datetime.time, or None if the dialog is cancelled.
|
||||
|
||||
"""
|
||||
|
||||
def time_validator(text):
|
||||
# Try them in order.
|
||||
for format in formats:
|
||||
try:
|
||||
dt = datetime.strptime(text, format)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
# This one worked; good enough for us.
|
||||
return (dt.time(), None)
|
||||
|
||||
# None of the formats worked.
|
||||
return (None, 'Please enter a valid time.')
|
||||
|
||||
# Add an example to the message?
|
||||
if show_example:
|
||||
message = message or ""
|
||||
message += "Current time in the correct format: " + datetime.now().strftime(formats[0])
|
||||
|
||||
return self.generic_entry(prompt, time_validator, message, **kwargs)
|
||||
|
||||
def datetime_entry(self, prompt, message=None, formats=['%x %X'], show_example=False, **kwargs):
|
||||
"""Prompt the user to enter a date and time.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
prompt: string
|
||||
Prompt to display to the user.
|
||||
message: string, optional
|
||||
Message to display under the entry line.
|
||||
formats: list of strings, optional
|
||||
The formats that the user can enter the date and time in. These
|
||||
should be format strings as accepted by the
|
||||
datetime.datetime.strptime() function from the standard library.
|
||||
They are tried in order, and the first that returns a datetime
|
||||
object without error is selected. Note that the '%x %X' in the
|
||||
default list is the current locale's date and time representation.
|
||||
show_example: Boolean
|
||||
If True, the current date and time in the first format given is appended to
|
||||
the message.
|
||||
|
||||
Returns
|
||||
-------
|
||||
datetime.datetime, or None if the dialog is cancelled.
|
||||
|
||||
"""
|
||||
|
||||
def datetime_validator(text):
|
||||
# Try them in order.
|
||||
for format in formats:
|
||||
try:
|
||||
dt = datetime.strptime(text, format)
|
||||
except ValueError:
|
||||
continue
|
||||
else:
|
||||
# This one worked; good enough for us.
|
||||
return (dt, None)
|
||||
|
||||
# None of the formats worked.
|
||||
return (None, 'Please enter a valid date and time.')
|
||||
|
||||
# Add an example to the message?
|
||||
if show_example:
|
||||
message = message or ""
|
||||
message += "Current date and time in the correct format: " + datetime.now().strftime(formats[0])
|
||||
|
||||
return self.generic_entry(prompt, datetime_validator, message, **kwargs)
|
||||
|
||||
def exit_with_error(self, error, **kwargs):
|
||||
"""Report an error and exit.
|
||||
|
||||
This raises a SystemExit exception to ask the interpreter to quit.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
error: string
|
||||
The error to report before quitting.
|
||||
|
||||
"""
|
||||
self.error(error, **kwargs)
|
||||
raise SystemExit(error)
|
||||
19
CodeSnippetRofi/entity/response.py
Normal file
19
CodeSnippetRofi/entity/response.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import json
|
||||
|
||||
|
||||
class SearchResponse:
|
||||
def __init__(self, response: dict):
|
||||
self.status = response["status"]
|
||||
self.data = [SearchResponse.SearchData(dict(r)) for r in json.loads(response["data"])]
|
||||
|
||||
class SearchData:
|
||||
def __init__(self, data: dict):
|
||||
self.id = data["id"]
|
||||
self.name = data["name"]
|
||||
self.path = data["path"]
|
||||
self.score = data["score"]
|
||||
|
||||
class NormalResponse:
|
||||
def __init__(self, response: dict):
|
||||
self.status = response["status"]
|
||||
self.data = response["data"]
|
||||
4
CodeSnippetRofi/entity/result.py
Normal file
4
CodeSnippetRofi/entity/result.py
Normal file
@@ -0,0 +1,4 @@
|
||||
class ActionResult:
|
||||
def __init__(self, ok: bool, message: str):
|
||||
self.ok = ok
|
||||
self.message = message
|
||||
58
CodeSnippetRofi/helper/api_helper.py
Normal file
58
CodeSnippetRofi/helper/api_helper.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
from common.constant import code_snippet_port, action_list, action_edit, action_delete, action_add
|
||||
from entity.response import SearchResponse, NormalResponse
|
||||
|
||||
|
||||
def _run_api(action: str, data: str) -> dict:
|
||||
action = action.upper()
|
||||
if action not in ["LIST", "ADD", "EDIT", "DELETE"]:
|
||||
raise ValueError("Invalid action")
|
||||
|
||||
message = {
|
||||
"action": f"{action}",
|
||||
"data": f"{data}"
|
||||
}
|
||||
|
||||
# 使用 Popen 创建管道
|
||||
echo_process = subprocess.Popen(
|
||||
["echo", json.dumps(message)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
nc_process = subprocess.Popen(
|
||||
["nc", "-q", "0", "127.0.0.1", code_snippet_port],
|
||||
stdin=echo_process.stdout,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True
|
||||
)
|
||||
|
||||
# 关闭 echo 的输出,避免资源泄漏
|
||||
echo_process.stdout.close()
|
||||
|
||||
try:
|
||||
# 获取 nc 的输出
|
||||
nc_stdout, nc_stderr = nc_process.communicate()
|
||||
return dict(json.loads(nc_stdout))
|
||||
except Exception:
|
||||
raise ConnectionError(f"后端交互失败!检查守护进程是否运行")
|
||||
|
||||
|
||||
def run_search(key: str) -> SearchResponse:
|
||||
return SearchResponse(_run_api(action_list, key))
|
||||
|
||||
|
||||
def run_edit(data: str) -> NormalResponse:
|
||||
return NormalResponse(_run_api(action_edit, data))
|
||||
|
||||
|
||||
def run_delete(path: str) -> NormalResponse:
|
||||
return NormalResponse(_run_api(action_delete, path))
|
||||
|
||||
|
||||
def run_add(data: str) -> NormalResponse:
|
||||
return NormalResponse(_run_api(action_add, data))
|
||||
334
CodeSnippetRofi/helper/file_helper.py
Normal file
334
CodeSnippetRofi/helper/file_helper.py
Normal file
@@ -0,0 +1,334 @@
|
||||
#!/usr/bin/env python3
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from common.constant import template_add, template_edit
|
||||
from entity.result import ActionResult
|
||||
from helper.api_helper import run_edit, run_add
|
||||
|
||||
|
||||
def _parse_add_text(text: str) -> str:
|
||||
# 提取第一个代码块(包含可选的 ```[Lang] 或 ```Lang)
|
||||
fence_re = re.search(r"```(?:\[(?P<fence_lang>[^\]]*)\]|(?P<fence_lang2>[A-Za-z0-9_+\-]+))?\s*\r?\n(?P<code>[\s\S]*?)```", text)
|
||||
fence_lang = ''
|
||||
content = ''
|
||||
if fence_re:
|
||||
fence_lang = (fence_re.group('fence_lang') or fence_re.group('fence_lang2') or '') or ''
|
||||
content = (fence_re.group('code') or '').strip()
|
||||
# 把模板占位符当做空值处理
|
||||
if fence_lang.strip().lower() == 'language' or fence_lang.strip() == '':
|
||||
fence_lang = ''
|
||||
else:
|
||||
fence_lang = fence_lang.strip()
|
||||
|
||||
# 定位 MetaData 区块(从"## MetaData"到文档末尾或下一个"## ")
|
||||
meta_match = re.search(r"##\s*MetaData\s*(?P<meta>[\s\S]*)", text, flags=re.I)
|
||||
meta = meta_match.group('meta') if meta_match else ''
|
||||
meta_lines = meta.splitlines()
|
||||
|
||||
def extract_list_value(field_name: str) -> str:
|
||||
field_name_re = re.compile(rf"^\s*-\s*{re.escape(field_name)}\s*$", flags=re.I)
|
||||
list_item_re = re.compile(r"^\s*-\s*(.*)$")
|
||||
for i, line in enumerate(meta_lines):
|
||||
if field_name_re.match(line):
|
||||
# 找下一条非空行作为子项
|
||||
j = i + 1
|
||||
while j < len(meta_lines) and meta_lines[j].strip() == "":
|
||||
j += 1
|
||||
if j < len(meta_lines):
|
||||
m = list_item_re.match(meta_lines[j])
|
||||
if m:
|
||||
val = m.group(1).strip()
|
||||
# 把模板占位符当做空值处理
|
||||
if val.lower() == field_name.lower() or val.lower() == 'language' and field_name.lower() == 'language' and val == '':
|
||||
return ''
|
||||
return val
|
||||
return ""
|
||||
return ""
|
||||
|
||||
name = extract_list_value("Name")
|
||||
language_meta = extract_list_value("Language")
|
||||
|
||||
# 优先使用 MetaData 下方的 Language,若无再使用代码块 fence 的语言
|
||||
language = language_meta or fence_lang or ""
|
||||
|
||||
entity = {
|
||||
"name": name or "",
|
||||
"language": language or "",
|
||||
"content": content or "",
|
||||
}
|
||||
return json.dumps(entity, ensure_ascii=False)
|
||||
|
||||
# 包装成读取文件的版本(保留原函数签名)
|
||||
def _parse_add(file_path: str) -> str:
|
||||
text = Path(file_path).read_text(encoding='utf-8')
|
||||
return _parse_add_text(text)
|
||||
|
||||
|
||||
def _prefill_edit(file_path: str, source_path: str, _id: str) -> None:
|
||||
# 将原始文件内容与元信息写入模板占位
|
||||
# 读取原文件
|
||||
src = Path(source_path).read_text(encoding='utf-8')
|
||||
|
||||
# 提取代码块(仅代码,不带多余模板)与语言
|
||||
code_block = re.search(r"```(?:\[([^\]]*)\]|([A-Za-z0-9_+\-]+))?\s*\r?\n([\s\S]*?)```", src)
|
||||
language = ''
|
||||
code_only = ''
|
||||
if code_block:
|
||||
language = (code_block.group(1) or code_block.group(2) or '').strip()
|
||||
code_only = (code_block.group(3) or '').strip()
|
||||
# 若未检测到代码块则回退为全文
|
||||
if not code_only:
|
||||
code_only = src.strip()
|
||||
|
||||
# 从源文档的 MetaData 段提取 tags 与 description
|
||||
tags: list[str] = []
|
||||
desc = ''
|
||||
# 定位 MetaData 段
|
||||
meta_match = re.search(r"##\s*MetaData([\s\S]*)", src)
|
||||
meta = meta_match.group(1) if meta_match else ''
|
||||
if meta:
|
||||
# Tags 段
|
||||
tags_section = re.search(r"-\s*Tags\s*([\s\S]*?)(?:\n-\s*Description|\Z)", meta)
|
||||
if tags_section:
|
||||
for line in tags_section.group(1).splitlines():
|
||||
m = re.match(r"^\s*-\s*(.+)$", line)
|
||||
if m:
|
||||
val = m.group(1).strip()
|
||||
if val:
|
||||
tags.append(val)
|
||||
# Description 段
|
||||
desc_section = re.search(r"-\s*Description\s*\n\s*-\s*(.+)", meta)
|
||||
if desc_section:
|
||||
desc = desc_section.group(1).strip()
|
||||
|
||||
# 若语言未能从代码块检测,则用路径上级名兜底
|
||||
if not language:
|
||||
language = Path(source_path).parent.name
|
||||
|
||||
# 重建模板(覆盖写入)
|
||||
rebuilt_lines = []
|
||||
rebuilt_lines.append("## Snippet\n\n")
|
||||
if language:
|
||||
rebuilt_lines.append(f"```{language}\n")
|
||||
else:
|
||||
rebuilt_lines.append("```\n")
|
||||
rebuilt_lines.append(code_only)
|
||||
rebuilt_lines.append("\n```\n\n")
|
||||
rebuilt_lines.append("## MetaData\n\n")
|
||||
rebuilt_lines.append("- Tags\n")
|
||||
if tags:
|
||||
for t in tags:
|
||||
rebuilt_lines.append(f" - {t}\n")
|
||||
else:
|
||||
rebuilt_lines.append(" - \n - \n")
|
||||
rebuilt_lines.append("- Description\n")
|
||||
rebuilt_lines.append(f" - {desc}\n")
|
||||
|
||||
Path(file_path).write_text(''.join(rebuilt_lines), encoding='utf-8')
|
||||
|
||||
|
||||
def _parse_edit(file_path: str, _id: str, source_path: str) -> str:
|
||||
text = Path(file_path).read_text(encoding='utf-8')
|
||||
# 解析语言(不强制需要)
|
||||
# 解析代码块内容
|
||||
code_match = re.search(r"```(?:\[[^\]]*\]|[A-Za-z0-9_+\-]*)\s*\r?\n([\s\S]*?)```", text)
|
||||
content = (code_match.group(1) if code_match else '').strip()
|
||||
# tags
|
||||
tag_lines = re.findall(r"^-\s+(.*)$", text, flags=re.M)
|
||||
# 在 '- Tags' 之后的两级缩进项
|
||||
tags_section = False
|
||||
tags = []
|
||||
for line in text.splitlines():
|
||||
if re.match(r"^-\s*Tags\s*$", line):
|
||||
tags_section = True
|
||||
continue
|
||||
if tags_section:
|
||||
m = re.match(r"^\s*-\s*(.*)$", line)
|
||||
if m:
|
||||
val = m.group(1).strip()
|
||||
if val:
|
||||
tags.append(val)
|
||||
else:
|
||||
# 离开 tags 段
|
||||
tags_section = False
|
||||
# 到 Description 再退出
|
||||
if re.match(r"^-\s*Description\s*$", line):
|
||||
tags_section = False
|
||||
|
||||
desc_match = re.search(r"-\s*Description\s*\n\s*-\s*(.*)", text)
|
||||
description = (desc_match.group(1) if desc_match else '').strip()
|
||||
|
||||
entity = {
|
||||
"id": _id,
|
||||
"path": source_path,
|
||||
"tags": tags,
|
||||
"description": description,
|
||||
"content": content,
|
||||
}
|
||||
return json.dumps(entity, ensure_ascii=False)
|
||||
|
||||
|
||||
def _create_tmp(content: str) -> str:
|
||||
# 创建临时文件,并写入template_path对应的文件中的内容,最终返回临时文件的路径
|
||||
# 临时文件路径固定为/tmp/<随机字符串>.md
|
||||
with tempfile.NamedTemporaryFile(prefix='', suffix='.md', dir='/tmp/', delete=False) as tmp_file:
|
||||
temp_path = tmp_file.name
|
||||
|
||||
# 将模板文件内容复制到临时文件
|
||||
Path(temp_path).write_text(content, encoding='utf-8')
|
||||
|
||||
return temp_path
|
||||
|
||||
|
||||
def _open_with_nvim(file_path: str) -> None:
|
||||
# 获取终端环境变量,默认为xterm
|
||||
term = os.environ.get('TERMINAL', '')
|
||||
if not term:
|
||||
term = 'xterm'
|
||||
|
||||
# 获取终端名称
|
||||
name = os.path.basename(term)
|
||||
|
||||
# 根据不同终端类型构建命令
|
||||
cmd = [term]
|
||||
|
||||
# 根据终端类型添加特定参数
|
||||
if name == 'alacritty':
|
||||
cmd.extend(['--class', 'floating-nvim', '-e', 'nvim', file_path])
|
||||
elif name == 'kitty':
|
||||
cmd.extend(['--class', 'floating-nvim', 'nvim', file_path])
|
||||
elif name == 'foot':
|
||||
cmd.extend(['-a', 'floating-nvim', 'nvim', file_path])
|
||||
elif name == 'wezterm':
|
||||
cmd.extend(['start', '--class', 'floating-nvim', '--', 'nvim', file_path])
|
||||
elif name.startswith('gnome-terminal'):
|
||||
cmd.extend(['--class=floating-nvim', '--', 'nvim', file_path])
|
||||
elif name == 'konsole':
|
||||
cmd.extend(['--class', 'floating-nvim', '-e', 'nvim', file_path])
|
||||
elif name == 'urxvt':
|
||||
cmd.extend(['-name', 'floating-nvim', '-e', 'nvim', file_path])
|
||||
elif name == 'xterm':
|
||||
cmd.extend(['-class', 'floating-nvim', '-e', 'nvim', file_path])
|
||||
else:
|
||||
cmd.extend(['-e', 'nvim', file_path])
|
||||
|
||||
# 阻塞执行命令,直到进程结束
|
||||
subprocess.run(cmd)
|
||||
|
||||
|
||||
def _copy(tmp_path: str) -> None:
|
||||
# 读取临时文件内容并提取代码块
|
||||
text = Path(tmp_path).read_text(encoding='utf-8')
|
||||
m = re.search(r"```(?:\[[^\]]*\]|[A-Za-z0-9_+\-]*)\s*\r?\n([\s\S]*?)```", text)
|
||||
if not m:
|
||||
code = ""
|
||||
else:
|
||||
code = m.group(1).strip()
|
||||
|
||||
# 检查是否有内容
|
||||
if not code:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
return
|
||||
|
||||
# 检查是否有 wl-copy 命令
|
||||
if not shutil.which('wl-copy'):
|
||||
print("Error: 未安装 wl-copy")
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
return
|
||||
|
||||
# 复制到剪贴板
|
||||
process = subprocess.Popen(['wl-copy'], stdin=subprocess.PIPE, text=True)
|
||||
process.communicate(input=code)
|
||||
|
||||
|
||||
def _calculate_file_sha256(file_path):
|
||||
"""
|
||||
计算文件的SHA256哈希值
|
||||
|
||||
Args:
|
||||
file_path (str): 文件路径
|
||||
|
||||
Returns:
|
||||
str: 文件的SHA256哈希值(十六进制字符串)
|
||||
"""
|
||||
# 创建SHA256哈希对象
|
||||
sha256_hash = hashlib.sha256()
|
||||
|
||||
# 以二进制模式打开文件
|
||||
with open(file_path, 'rb') as file:
|
||||
# 分块读取文件,避免内存问题
|
||||
for byte_block in iter(lambda: file.read(4096), b""):
|
||||
sha256_hash.update(byte_block)
|
||||
|
||||
# 返回十六进制格式的哈希值
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
def edit_and_copy(path: str, id: str) -> ActionResult:
|
||||
tmp_path = _create_tmp(template_edit)
|
||||
try:
|
||||
_prefill_edit(tmp_path, path, id)
|
||||
_open_with_nvim(tmp_path)
|
||||
_copy(tmp_path)
|
||||
# 清理临时文件
|
||||
return ActionResult(True, "")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return ActionResult(False, str(e))
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
def edit(path: str, id: str) -> ActionResult:
|
||||
tmp_path = _create_tmp(template_edit)
|
||||
primary_hash = _calculate_file_sha256(path)
|
||||
try:
|
||||
_prefill_edit(tmp_path, path, id)
|
||||
_open_with_nvim(tmp_path)
|
||||
new_hash = _calculate_file_sha256(path)
|
||||
if primary_hash == new_hash:
|
||||
return ActionResult(True, "文件未修改")
|
||||
edit_json = _parse_edit(tmp_path, id, path)
|
||||
response = run_edit(edit_json)
|
||||
if response.status == "SUCCESS":
|
||||
return ActionResult(True, response.data)
|
||||
else:
|
||||
return ActionResult(False, response.data)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return ActionResult(False, str(e))
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def add() -> ActionResult:
|
||||
tmp_path = _create_tmp(template_add)
|
||||
primary_hash = _calculate_file_sha256(tmp_path)
|
||||
try:
|
||||
_open_with_nvim(tmp_path)
|
||||
new_hash = _calculate_file_sha256(tmp_path)
|
||||
if primary_hash == new_hash:
|
||||
return ActionResult(True, "内容为空")
|
||||
add_json = _parse_add(tmp_path)
|
||||
response = run_add(add_json)
|
||||
if response.status == "SUCCESS":
|
||||
return ActionResult(True, response.data)
|
||||
else:
|
||||
return ActionResult(False, response.data)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return ActionResult(False, str(e))
|
||||
finally:
|
||||
Path(tmp_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
tmp_path = _create_tmp(template_add)
|
||||
_open_with_nvim(tmp_path)
|
||||
add_json = _parse_add(tmp_path)
|
||||
print(add_json)
|
||||
11
CodeSnippetRofi/launcher.py
Executable file
11
CodeSnippetRofi/launcher.py
Executable file
@@ -0,0 +1,11 @@
|
||||
import os
|
||||
|
||||
from common.constant import code_snippet_rofi
|
||||
from common import rofi
|
||||
from menu.MainMenu import MainMenu
|
||||
|
||||
if code_snippet_rofi is None:
|
||||
r = rofi.Rofi()
|
||||
else:
|
||||
r = rofi.Rofi(code_snippet_rofi)
|
||||
MainMenu(r).run()
|
||||
13
CodeSnippetRofi/menu/AddMenu.py
Normal file
13
CodeSnippetRofi/menu/AddMenu.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import rofi
|
||||
|
||||
from helper.file_helper import add
|
||||
|
||||
|
||||
class AddMenu:
|
||||
def __init__(self,r:rofi.Rofi):
|
||||
self._r = r
|
||||
|
||||
def run(self):
|
||||
result = add()
|
||||
if not result.ok:
|
||||
self._r.error(result.message)
|
||||
44
CodeSnippetRofi/menu/DeleteMenu.py
Normal file
44
CodeSnippetRofi/menu/DeleteMenu.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import rofi
|
||||
|
||||
from helper.api_helper import run_search, run_delete
|
||||
|
||||
|
||||
class DeleteMenu:
|
||||
def __init__(self,r: rofi.Rofi):
|
||||
self._r = r
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
result = run_search("")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
self._r.error(str(e))
|
||||
return
|
||||
if result.status != "SUCCESS":
|
||||
self._r.error(result.data)
|
||||
return
|
||||
while True:
|
||||
if len(result.data) == 0:
|
||||
self._r.select("删除", ["未找到Snippet记录"])
|
||||
break
|
||||
options = [f"{d.name} - {d.path}" for d in result.data]
|
||||
index, key = self._r.select("删除", options)
|
||||
if key == -1:
|
||||
break
|
||||
ConfirmMenu(self._r,result.data[index].path).run()
|
||||
|
||||
|
||||
class ConfirmMenu:
|
||||
def __init__(self,r:rofi.Rofi, path: str):
|
||||
self._r = r
|
||||
self.path = path
|
||||
|
||||
def run(self):
|
||||
options = ["是", "否"]
|
||||
index, key = self._r.select("确定删除?", options)
|
||||
if key == -1:
|
||||
return
|
||||
if index == 0:
|
||||
res = run_delete(self.path)
|
||||
if res.status != "SUCCESS":
|
||||
self._r.error(res.data)
|
||||
32
CodeSnippetRofi/menu/EditMenu.py
Normal file
32
CodeSnippetRofi/menu/EditMenu.py
Normal file
@@ -0,0 +1,32 @@
|
||||
import rofi
|
||||
|
||||
from helper.api_helper import run_search
|
||||
from helper.file_helper import edit
|
||||
|
||||
|
||||
class EditMenu:
|
||||
def __init__(self,r: rofi.Rofi):
|
||||
self._r = r
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
result = run_search("")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
self._r.error(str(e))
|
||||
return
|
||||
if result.status != "SUCCESS":
|
||||
self._r.error(result.data)
|
||||
return
|
||||
while True:
|
||||
if len(result.data) == 0:
|
||||
self._r.select("编辑", ["未找到Snippet记录"])
|
||||
break
|
||||
options = [f"{d.name} - {d.path}" for d in result.data]
|
||||
index, key = self._r.select("编辑", options)
|
||||
if key == -1:
|
||||
break
|
||||
data = result.data[index]
|
||||
res = edit(data.path, data.id)
|
||||
if not res.ok:
|
||||
self._r.error(res.message)
|
||||
29
CodeSnippetRofi/menu/MainMenu.py
Normal file
29
CodeSnippetRofi/menu/MainMenu.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import rofi
|
||||
|
||||
from menu.AddMenu import AddMenu
|
||||
from menu.DeleteMenu import DeleteMenu
|
||||
from menu.EditMenu import EditMenu
|
||||
from menu.SearchMenu import SearchMenu
|
||||
|
||||
|
||||
class MainMenu:
|
||||
def __init__(self,r: rofi.Rofi):
|
||||
self._r = r
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
options = ["搜索", "编辑", "添加", "删除"]
|
||||
index, key = self._r.select("Code Snippet", options)
|
||||
print(f"index: {index}")
|
||||
print(f"key: {key}")
|
||||
if key == -1:
|
||||
break
|
||||
match index:
|
||||
case 0:
|
||||
SearchMenu(self._r).run()
|
||||
case 1:
|
||||
EditMenu(self._r).run()
|
||||
case 2:
|
||||
AddMenu(self._r).run()
|
||||
case 3:
|
||||
DeleteMenu(self._r).run()
|
||||
71
CodeSnippetRofi/menu/SearchMenu.py
Normal file
71
CodeSnippetRofi/menu/SearchMenu.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import rofi
|
||||
|
||||
from entity.response import SearchResponse
|
||||
from helper.api_helper import run_search, run_delete
|
||||
from helper.file_helper import edit, edit_and_copy
|
||||
|
||||
|
||||
class SearchMenu:
|
||||
def __init__(self, r: rofi.Rofi):
|
||||
self._r = r
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
key = self._r.text_entry("搜索", allow_blank=True)
|
||||
if key is None:
|
||||
break
|
||||
print(key)
|
||||
try:
|
||||
res = run_search(key)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
self._r.error(str(e))
|
||||
return
|
||||
if res.status != "SUCCESS":
|
||||
self._r.error(res.data)
|
||||
return
|
||||
SearchResultMenu(self._r, res.data).run()
|
||||
|
||||
|
||||
class SearchResultMenu:
|
||||
def __init__(self, r: rofi.Rofi, data: list[SearchResponse.SearchData]):
|
||||
self._r = r
|
||||
self._data = data
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
if len(self._data) == 0:
|
||||
self._r.select("搜索结果", ["未找到Snippet记录"])
|
||||
break
|
||||
else:
|
||||
# 将data按照索引和path换成dict,前者为键
|
||||
options = [f"{d.name} - {d.path}" for d in self._data]
|
||||
index, key = self._r.select("搜索结果", options)
|
||||
if key == -1:
|
||||
break
|
||||
SearchResultActionMenu(self._r, self._data[index]).run()
|
||||
|
||||
|
||||
class SearchResultActionMenu:
|
||||
def __init__(self, r: rofi.Rofi, data: SearchResponse.SearchData):
|
||||
self._r = r
|
||||
self.data = data
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
index, key = self._r.select("操作", ["编辑", "编辑并复制", "删除"])
|
||||
if key == -1:
|
||||
break
|
||||
match index:
|
||||
case 0:
|
||||
result = edit(self.data.path, self.data.id)
|
||||
if not result.ok:
|
||||
self._r.error(result.message)
|
||||
case 1:
|
||||
result = edit_and_copy(self.data.path, self.data.id)
|
||||
if not result.ok:
|
||||
self._r.error(result.message)
|
||||
exit()
|
||||
case 2:
|
||||
run_delete(self.data.path)
|
||||
break
|
||||
Reference in New Issue
Block a user