Dup Ver Goto 📝

RecentUrlList

PT2/lang/python/qt/pyside6/examples does not exist
To
520 lines, 1514 words, 15250 chars Page 'RecentUrlList' does not exist.

This is a very simple quick and dirty app using a QTableWidget to display a list of urls copied to the clipboard since the program was started. It relies on the pp script, which is a wrapper around e.g. pbpaste on macos, or xsel on Linux. Clicking on a table cell runs a command with the url as the argument. In this case it is f3c, which sets the url of the front Chrome tab on my mac mini via osascript.

Health Warning This is far from robust. It is a quick bodge to scratch an itch, like a lot of stuff I write. But it serves as simple examples for a number of things with Python and Qt. If I find myself using it a lot, then based on how I use it, I'll think out how to rewrite it. This is the initial bodge I wrote after thinking 'what was that url' this morning, viz, an app that watches the clipboard for urls. The other itch to scratch was to have an easy way of telling the mac mini connected to my TV to play a given youtube url.

Current Version

This takes the command from the environment variable CMD, which is expected to take the url as its only argument. (Anything clever should be done in a script that CMD points to.) It could easily be modified to pass both url and title, but I haven't found a use for that yet, so haven't bothered.

There are three keyboard shortcuts: C-c to clear, s to save, and l to load. It uses JSON files to store data.

#!/usr/bin/env python
from icecream import ic; ic.configureOutput(includeContext=True)


import sys
import re
import subprocess
import requests
import os
import json
from threading import Thread
import PySide6.QtCore
import PySide6.QtGui
import PySide6.QtWidgets
import PySide6.QtNetwork
app = PySide6.QtWidgets.QApplication()

CMD = os.getenv("CMD","f3c")

def copy(x):
  subprocess.run(["pc"],input=x.encode())

def paste():
  m = subprocess.run(["pp"],capture_output=True)
  f = sys.stderr
  if (x := m.returncode) > 0:
    out = m.stdout.splitlines()
    err = m.stderr.splitlines()
    print(f"pp returned {x}:",file=f)
    print(f"stdout:",file=f)
    for line in out:
      line = b"  " + line + b"\n"
      f.buffer.write(line)
    print(f"stderr:",file=f)
    for line in err:
      line = b"  " + line + b"\n"
      f.buffer.write(line)
    return ""
  else:
    return m.stdout.decode()

class Url:
  def __init__(self,url,title=None):
    self.url = url
    self.title = title

class UrlTable(PySide6.QtWidgets.QTableWidget):
  def __init__(self,*xs,**kw):
    super().__init__(*xs,**kw)
    self.parent = xs[0]

  def mousePressEvent(self, event):
    ic(event)
    if event.button() == PySide6.QtCore.Qt.LeftButton:
      it = self.itemAt(event.pos())
      if it is not None:
        row = it.row()
        self.parent.clicked_row(row,event)

  def keyPressEvent(self,event):
    self.parent.keyPressEvent(event)

class UrlList(PySide6.QtWidgets.QWidget):
  def __init__(self,init_urls):
    super().__init__()
    #self.table_widget = PySide6.QtWidgets.QTableWidget(self)
    self.table_widget = UrlTable(self)
    self.resize(1300,600)
    self.urlset = set()
    self.urls = []

    self.urlfont = PySide6.QtGui.QFont("Hack Nerd Font Mono",14)
    self.titlefont = PySide6.QtGui.QFont("Optima",18)
    self.urlcolor = PySide6.QtGui.QColor("#aaffaa")
    self.titlecolor = PySide6.QtGui.QColor("#aaaaff")
    #self.urlStyle = 'QTableWidgetItem { font-family: "Hack Nerd Font Mono"; font-size: 14px; color: red; }'
    #self.titleStyle = 'QTableWidgetItem { font-family: "Optima"; font-size: 18px; color: yellow; }'

    for url in init_urls:
      if re.match(r"https?://",url):
        if not url in self.urlset:
          self.urlset.add(url)
          self.urls.append(Url(url))
    table = self.table_widget
    table.setHorizontalHeaderLabels(["URL","Title"])
    table.setRowCount(0)
    table.setColumnCount(2)
    table.resize(self.rect().width(),self.rect().height())
    self.update_table()

    self.timer = PySide6.QtCore.QTimer()
    timer = self.timer
    timer.timeout.connect(self.tick)
    timer.setInterval(1000)
    timer.start()

    #table.cellClicked.connect(self.cell_clicked)
    table.setSelectionMode(PySide6.QtWidgets.QTableWidget.NoSelection)

  def cell_clicked(self,row,column):
    url = self.urls[row]
    print(row,column,url)
    subprocess.run(["f3c",url.url])

  def keyPressEvent(self,e):
    key = e.key()
    ic("key",e)
    key = PySide6.QtGui.QKeySequence(key).toString().lower() 
    mods = e.modifiers()
    match key:
      case "c":
        if mods == PySide6.QtCore.Qt.ControlModifier:
          self.urlset = set()
          self.urls = []
          self.update_table()
          return
      case "s":
        self.save_urls()
        return
      case "l":
        self.load_urls()
        return

  def save_urls(self):
    file_path, _ = PySide6.QtWidgets.QFileDialog.getSaveFileName(self, "Save File", "", "JSON Files (*.json)")

    # If the user selected a path, print it or handle it as needed
    if file_path:
      try:
        data = []
        for u in self.urls:
          data.append([u.url,u.title])
        with open(file_path,"wt") as f:
          json.dump(data,f)
        PySide6.QtWidgets.QMessageBox.information(self,"Saved Successfully",f"Saved to {file_path}")
      except Exception as e:
        msg = f"Exception {e} saving to {file_path}"
        print(msg)
        PySide6.QtWidgets.QMessageBox.critical(self,"Failed to save",msg)

  def message_box(self,msg):
    PySide

  def load_urls(self):
    file_path, _ = PySide6.QtWidgets.QFileDialog.getOpenFileName(self, "Load File", "", "JSON Files (*.json)")

    # If the user selected a path, print it or handle it as needed
    if file_path:
      try:
        if not os.path.exists(file_path):
          msg = f"File '{file_path}' does not exist"
          PySide6.QtWidgets.QMessageBox.critical(self,"Failed to load",msg)
          return
        with open(file_path) as f:
          data = json.load(f)
        newurls = []
        newurlset = set()
        if type(data) is not list:
          raise Exception("JSON data must be array")
        for item in data:
          if not type(item) is list:
            raise Exception("JSON item must be array")
          if not len(item) == 2:
            raise Exception("JSON item must be array with 2 items")
          url, title = item
          if type(url) is not str:
            raise Exception(f"URL must be string, got {url} ({type(url)})")
          if type(title) is not str:
            raise Exception(f"Title must be string, got {title} ({type(title)})")
          if not url in newurlset:
            newurls.append(Url(url,title))
            newurlset.add(url)
        self.urls = newurls
        self.urlset = newurlset
        self.update_table()
        PySide6.QtWidgets.QMessageBox.information(self,"Loaded Successfully",f"Loaded from {file_path}")
      except Exception as e:
        msg = f"Exception {e} loading from {file_path}"
        print(msg)
        PySide6.QtWidgets.QMessageBox.critical(self,"Failed to load",msg)

  def clicked_row(self,row,event):
    mods = event.modifiers()
    if mods == PySide6.QtCore.Qt.NoModifier:
      url = self.urls[row]
      subprocess.run([CMD,url.url])
    elif mods == PySide6.QtCore.Qt.ControlModifier:
      url = self.urls[row]
      copy(url.url)
    elif mods == PySide6.QtCore.Qt.AltModifier:
      url = self.urls[row]
      self.urls.pop(row)
      if url.url in self.urlset:
        self.urlset.remove(url.url)
      self.update_table()
    else:
      print("Other")
    print("click",row,mods)

  def tick(self):
    p = paste()
    if re.match(r"https?://",p):
      if not p in self.urlset:
        self.urlset.add(p)
        self.urls.append(Url(p))
        self.update_table()
    for i,url in enumerate(self.urls):
      title = url.title 
      if title is None:
        print(f"Fetch title for {url.url}")
        thread = Thread(target=self.get_title_for(url))
        thread.start()

  def update_table(self):
    table = self.table_widget
    table.setRowCount(len(self.urls))
    for i,url in enumerate(self.urls):
      title = url.title 
      if title is None:
        title = "No title"
      table.setItem(i,0,x := PySide6.QtWidgets.QTableWidgetItem(url.url))
      table.setItem(i,1,y := PySide6.QtWidgets.QTableWidgetItem(title))
      x.setFont(self.urlfont)
      y.setFont(self.titlefont)
      x.setForeground(self.urlcolor)
      y.setForeground(self.titlecolor)

    table.resizeColumnsToContents()
    self.update()

  def resizeEvent(self,e):
    self.table_widget.resize(self.rect().width(),self.rect().height())
    self.table_widget.resizeColumnsToContents()
    super().resizeEvent(e)

  def get_title_for(self,url):
    print(f"Getting title for {url.url}")
    r = requests.get(url.url)
    print(f"Got reply for for {url.url}")
    t = r.text.replace("\r","").replace("\n"," ")
    m = re.search(r"<title[^>]*>(.*?)</title>",t,re.I)
    if not m:
      url.title = "No title"
    else:
      url.title = m.group(1)
    self.update_table()

init_urls = []
args = sys.argv[1:]
stdin = None
for arg in args:
  if re.match(r"https?://",arg):
    init_urls.append(arg)
  elif arg == "-":
    if stdin is None:
      stdin = sys.stdin.read().rstrip().splitlines()
      for line in stdin:
        if re.match(r"https?://",line):
          init_urls.append(line)
  elif os.path.isfile(arg):
    try:
      with open(arg) as f:
        lines = f.read().rstrip().splitlines()
        for line in lines:
          if re.match(r"https?://",line):
            init_urls.append(line)
    except Exception as e:
      print(f"Exception {e} ({type(e)}) opening {arg}")
  else:
    print(f"Can't deal with {arg}")

main = UrlList(init_urls)
main.show()
app.exec()

Older Version

import sys
import re
import subprocess
import requests
import os
from threading import Thread
import PySide6.QtCore
import PySide6.QtGui
import PySide6.QtWidgets
import PySide6.QtNetwork
app = PySide6.QtWidgets.QApplication()

def paste():
  m = subprocess.run(["pp"],capture_output=True)
  f = sys.stderr
  if (x := m.returncode) > 0:
    out = m.stdout.splitlines()
    err = m.stderr.splitlines()
    print(f"pp returned {x}:",file=f)
    print(f"stdout:",file=f)
    for line in out:
      line = b"  " + line + b"\n"
      f.buffer.write(line)
    print(f"stderr:",file=f)
    for line in err:
      line = b"  " + line + b"\n"
      f.buffer.write(line)
    return ""
  else:
    return m.stdout.decode()

class F3Cell(PySide6.QtWidgets.QTableWidgetItem):
  def __init__(self,label,href=None):
    super().__init__(label)
    if href is None:
      href = label
    self.href = href

  def mousePressEvent(self,e):
    button = e.button()
    if button == PySide6.QtGui.LeftButton:
      subprocess.run(["f3c",self.href])
    elif button == PySide6.QtGui.RightButton:
      subprocess.run(["pce",self.href])

class Url:
  def __init__(self,url,title=None):
    self.url = url
    self.title = title

class UrlList(PySide6.QtWidgets.QWidget):
  def __init__(self,init_urls):
    super().__init__()
    self.table_widget = PySide6.QtWidgets.QTableWidget(self)
    self.resize(1300,600)
    self.urlset = set()
    self.urls = []

    self.urlfont = PySide6.QtGui.QFont("Hack Nerd Font Mono",14)
    self.titlefont = PySide6.QtGui.QFont("Optima",18)

    for url in init_urls:
      if re.match(r"https?://",url):
        if not url in self.urlset:
          self.urlset.add(url)
          self.urls.append(Url(url))
    table = self.table_widget
    table.setHorizontalHeaderLabels(["URL","Title"])
    table.setRowCount(0)
    table.setColumnCount(2)
    table.resize(self.rect().width(),self.rect().height())
    self.update_table()

    self.timer = PySide6.QtCore.QTimer()
    timer = self.timer
    timer.timeout.connect(self.tick)
    timer.setInterval(1000)
    timer.start()

    table.cellClicked.connect(self.cell_clicked)
    table.setSelectionMode(PySide6.QtWidgets.QTableWidget.NoSelection)

  def cell_clicked(self,row,column):
    url = self.urls[row]
    print(row,column,url)
    subprocess.run(["f3c",url.url])

  def tick(self):
    p = paste()
    if re.match(r"https?://",p):
      if not p in self.urlset:
        self.urlset.add(p)
        self.urls.append(Url(p))
        self.update_table()
    for i,url in enumerate(self.urls):
      title = url.title 
      if title is None:
        print(f"Fetch title for {url.url}")
        thread = Thread(target=self.get_title_for(url))
        thread.start()

  def update_table(self):
    table = self.table_widget
    table.setRowCount(len(self.urls))
    for i,url in enumerate(self.urls):
      title = url.title 
      if title is None:
        title = "No title"
      table.setItem(i,0,x := F3Cell(url.url))
      table.setItem(i,1,y := F3Cell(title))
      x.setFont(self.urlfont)
      y.setFont(self.titlefont)

    table.resizeColumnsToContents()
    self.update()

  def resizeEvent(self,e):
    self.table_widget.resize(self.rect().width(),self.rect().height())
    self.table_widget.resizeColumnsToContents()
    super().resizeEvent(e)

  def get_title_for(self,url):
    print(f"Getting title for {url.url}")
    r = requests.get(url.url)
    print(f"Got reply for for {url.url}")
    t = r.text.replace("\r","").replace("\n"," ")
    m = re.search(r"<title[^>]*>(.*?)</title>",t,re.I)
    if not m:
      url.title = "No title"
    else:
      url.title = m.group(1)
    self.update_table()

init_urls = []
args = sys.argv[1:]
stdin = None
for arg in args:
  if re.match(r"https?://",arg):
    init_urls.append(arg)
  elif arg == "-":
    if stdin is None:
      stdin = sys.stdin.read().rstrip().splitlines()
      for line in stdin:
        if re.match(r"https?://",line):
          init_urls.append(line)
  elif os.path.isfile(arg):
    try:
      with open(arg) as f:
        lines = f.read().rstrip().splitlines()
        for line in lines:
          if re.match(r"https?://",line):
            init_urls.append(line)
    except Exception as e:
      print(f"Exception {e} ({type(e)}) opening {arg}")
  else:
    print(f"Can't deal with {arg}")

main = UrlList(init_urls)
main.show()
app.exec()

pp

#!/bin/bash
if [ -n "$WAYLAND_DISPLAY" ]; then
  paste() { wl-paste; }
elif [ -n "$DISPLAY" ]; then # X11
  paste() { xsel -o -b; }
elif [ -d "/Applications" ]; then # macos
  paste() { pbpaste; }
elif [ -d "/cygdrive/c/cygwin64" ]; then # cygwin
  paste() { cat /dev/clipboard; }
else
  echo "Cannot paste as not gui" 1>&2
  exit 1
fi
if [ $# = 0 ]; then
  paste
else
  for s; do
    if [ -n "$s" ]; then
      if [ -e "$1" ]; then
        if [ -z "$CLOBBER" ] && [ -o noclobber ]; then
          echo "$1 exists, not overwriting"
          continue
        fi
      fi
      paste | tee "$1"
    else
      echo "Blank argument"
    fi
  done
fi

osascript

#!/usr/bin/env python
import subprocess
import sys
args = sys.argv[1:]
if len(args) == 0:
  print(f"{sys.argv[0]} url")
  exit(1)
url = args[0]
if '"' in url:
  print(f"Url can't contain \"")
  exit(1)
script = f"""tell application "Google Chrome"
  tell front window
    set URL of active tab to "{url}"
  end tell
end tell
"""
subprocess.run(["osascript"],input=script.encode())