From 96fe1ceaf89b5e1fa8ad1c35be84e84ab393b7ac Mon Sep 17 00:00:00 2001 From: Sören Tempel Date: Mon, 24 May 2021 00:24:45 +0200 Subject: Add preliminary support for executable-specific history The current executable is determined by using tcgetpgrp on the PTY file descriptor. All history entries are stored in an SQL lite database on a per-line basis. --- saneterm/history.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ saneterm/keys.py | 3 +++ saneterm/proc.py | 14 ++++++++++++ saneterm/terminal.py | 28 ++++++++++++++++++++++++ 4 files changed, 107 insertions(+) create mode 100644 saneterm/history.py create mode 100644 saneterm/proc.py diff --git a/saneterm/history.py b/saneterm/history.py new file mode 100644 index 0000000..f57155c --- /dev/null +++ b/saneterm/history.py @@ -0,0 +1,62 @@ +import os +import sqlite3 + +from . import proc + +HISTORY_FN = "history.db" + +class History(): + __schema = """ + CREATE TABLE IF NOT EXISTS history (exe TEXT, entry TEXT) + """ + + def __init__(self): + # XXX: Maybe consult os.environ["HISTFILE"] + if "XDG_DATA_DIR" in os.environ: + data_dir = os.environ["XDG_DATA_DIR"] + else: + data_dir = os.path.join(os.path.expanduser('~'), '.local', 'share') + + data_dir = os.path.join(data_dir, "saneterm") + os.makedirs(data_dir, exist_ok=True) + histfile = os.path.join(data_dir, HISTORY_FN) + + self.__con = sqlite3.connect(histfile) + self.__cur = self.__con.cursor() + + self.__cur.execute(self.__schema) + + def close(self): + self.__con.close() + + def add_entry(self, fd, entry): + exe = self.__get_exec(fd) + + # Strip trailing newline (if any). + if len(entry) == 0: + return + elif entry[-1] == '\n': + entry = entry[0:-1] + + self.__cur.execute("INSERT INTO history VALUES (?, ?)", (exe, entry)) + self.__con.commit() + + # TODO: Delete old entries + + def get_entry(self, fd, relidx): + exe = self.__get_exec(fd) + + self.__cur.execute(""" + SELECT entry FROM history WHERE exe=:exe LIMIT 1 OFFSET + (( SELECT count(*) FROM history WHERE exe=:exe ) - :relidx); + """, {"exe": exe, "relidx": relidx}) + + res = self.__cur.fetchone() + if res is None: + return None + + return res[0] + + def __get_exec(self, fd): + pid = os.tcgetpgrp(fd); + return proc.executable(pid) diff --git a/saneterm/keys.py b/saneterm/keys.py index 47058f6..5572f71 100644 --- a/saneterm/keys.py +++ b/saneterm/keys.py @@ -24,6 +24,9 @@ class Bindings(): bind "w" { "delete-from-cursor" (word-ends, -1) }; bind "h" { "backspace" () }; + bind "Up" { "history-entry" (1) }; + bind "Down" { "history-entry" (-1) }; + /* Since c is used for VINTR, unbind v */ unbind "v"; } diff --git a/saneterm/proc.py b/saneterm/proc.py new file mode 100644 index 0000000..7fd5b72 --- /dev/null +++ b/saneterm/proc.py @@ -0,0 +1,14 @@ +import os + +# This code is highly linux-specific and requires proc to be mounted in +# order to retrieve information about the current foreground process. + +def __proc_dir(pid): + return os.path.join("/proc", str(pid)) + +def executable(pid): + exec = os.path.join(__proc_dir(pid), "exe") + dest = os.readlink(exec) + + # XXX: Does this work with busybox symlinks i.e. /bin/sh → /bin/busybox? + return os.path.abspath(dest) diff --git a/saneterm/terminal.py b/saneterm/terminal.py index ebae1db..e31a8ce 100644 --- a/saneterm/terminal.py +++ b/saneterm/terminal.py @@ -7,6 +7,7 @@ import fcntl import struct from . import keys +from .history import History from .termview import * import gi @@ -63,6 +64,9 @@ class Terminal(Gtk.Window): Gtk.Window.__init__(self, title=NAME) self.set_name(NAME) + self.hist = History() + self.reset_history_index() + self.pty = PtySource(cmd) self.pty.set_callback(self.handle_pty) self.pty.attach(None) @@ -77,7 +81,9 @@ class Terminal(Gtk.Window): self.termview.connect("termios-ctrlkey", self.termios_ctrl) self.termview.connect("size-allocate", self.autoscroll) self.termview.connect("populate-popup", self.populate_popup) + self.connect("configure-event", self.update_size) + self.connect("destroy", self.destroy) bindings = keys.Bindings(self.termview) for key, idx in keys.CTRL.items(): @@ -89,6 +95,14 @@ class Terminal(Gtk.Window): self.scroll.add(self.termview) self.add(self.scroll) + GObject.signal_new("history-entry", self.termview, + GObject.SIGNAL_ACTION, GObject.TYPE_NONE, + (GObject.TYPE_LONG,)) + self.termview.connect("history-entry", self.history) + + def destroy(self, widget): + self.hist.close() + def update_wrapmode(self): mode = Gtk.WrapMode.WORD_CHAR if self.config['wordwrap'] else Gtk.WrapMode.NONE self.termview.set_wrap_mode(mode) @@ -129,6 +143,17 @@ class Terminal(Gtk.Window): self.termview.insert_data(self.decoder.decode(data)) return GLib.SOURCE_CONTINUE + def reset_history_index(self): + self.hist_index = 0 + + def history(self, termview, idx): + self.hist_index += idx + entry = self.hist.get_entry(self.pty.master, self.hist_index) + + if not entry is None: + self.termview.emit("kill-after-output") + self.termview.emit("insert-at-cursor", entry) + def autoscroll(self, widget, rect): if not self.config['autoscroll']: return @@ -155,6 +180,9 @@ class Terminal(Gtk.Window): popup.show_all() def user_input(self, termview, line): + self.hist.add_entry(self.pty.master, line) + self.reset_history_index() + os.write(self.pty.master, line.encode("UTF-8")) def termios_ctrl(self, termview, cidx): -- cgit 1.4.1