about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSören Tempel <soeren+git@soeren-tempel.net>2021-05-24 00:24:45 +0200
committerSören Tempel <soeren+git@soeren-tempel.net>2021-05-24 00:24:45 +0200
commit96fe1ceaf89b5e1fa8ad1c35be84e84ab393b7ac (patch)
tree7b45624e96a55f76102d0e2b50d31379060ec107
parenta7dceab464dbac7fa51e932373170ed8847a5be5 (diff)
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.
-rw-r--r--saneterm/history.py62
-rw-r--r--saneterm/keys.py3
-rw-r--r--saneterm/proc.py14
-rw-r--r--saneterm/terminal.py28
4 files changed, 107 insertions, 0 deletions
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 "<ctrl>w" { "delete-from-cursor" (word-ends, -1) };
             bind "<ctrl>h" { "backspace" () };
 
+            bind "Up" { "history-entry" (1) };
+            bind "Down" { "history-entry" (-1) };
+
             /* Since <ctrl>c is used for VINTR, unbind <ctrl>v */
             unbind "<ctrl>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):