• =?utf-8?Q?Tutorial=3A_Firefox_right=E2=80=91click_extension_+?= =?utf-8?Q?_native_host_on_Windows_=28GUI_launch_mystery=29?=

    From Maria Sophia@mariasophia@comprehension.com to alt.comp.software.firefox,alt.comp.os.windows-10,alt.os.linux on Mon Feb 23 21:48:15 2026
    From Newsgroup: alt.os.linux

    Tutorial:
    Firefox right-click extension + native host on Windows (GUI launch mystery)

    Today I invested a couple of hours learning how to add a right-click
    context menu to Firefox to call a Windows $EDITOR to edit source HTML.

    I didn't *need* to write the extension, but since it was my very first
    Firefox extension, I learned a ton I'd like to discuss with those who are interested in writing their own Firefox extensions on their given platform.

    This tutorial can be rightly titled something like:
    *Adding a right-click Firefox menu item that calls a native program*

    This tutorial shows how to write a simple Firefox extension on Windows that adds a right-click context menu item, extracts the HTML of the current page
    & sends it to a native Windows program. The native program receives the

    HTML, writes it to a temporary file, and attempts to launch an editor such
    as Notepad++ or gVim.

    Everything in this tutorial worked end-to-end on my Windows 10 box,
    except for one final detail, where Windows does not (yet) allow GUI applications to appear on the user's desktop when launched from a Firefox native messaging host. The editor launch command executes successfully, the temp file is created, everything important is logged in great detail, and
    the native host returns success, but the editor window never appears.

    This might be a Windows session isolation behavior.
    Linux users may not encounter this limitation.

    We know Firefox, even on Windows, *can* launch any desired $EDITOR.
    about:config > view_source.editor
    view_source.editor.external = true
    view_source.editor.path = C:\app\editor\txt\vi\gvim.exe

    So why can't we get an extension to do the same thing native in Firefox?

    Dunno yet. So this tutorial is only 99% complete but it's still useful
    because it demonstrates the entire Firefox-extension pipeline, the native messaging host mechanism, and the Windows registry integration.

    Only the final GUI launch behavior remains an open question.
    Note that all paths are mine, as they're copied from my working setup.
    The paths don't matter as long as they fit what your system filespecs are.

    1. Overview

    A. What the extension does
    a. Adds a right-click context menu item to Firefox.
    b. Extracts the HTML of the current page.
    c. Sends the HTML to a native Windows program.
    d. The native program writes the HTML to a temp file.
    e. The native program attempts to launch an editor.

    B. What works
    a. The extension loads.
    b. The context menu appears.
    c. The content script extracts HTML.
    d. The background script sends the HTML to the native host.
    e. The native host receives JSON correctly.
    f. The Python script writes the temp file.
    g. The Python script executes the editor launch command.
    h. The native host returns success.
    i. Every important step is logged to debug files.

    C. What remains open
    a. On Windows, GUI editors launched from a native host do not appear.
    b. No process appears in tasklist.
    c. No error is thrown.
    d. Yet, the same editor launches normally when invoked by
    Firefox's built-in view_source.editor.external mechanism.
    e. Linux users may not have this limitation. Dunno.

    2. Directory Setup (my filespecs are used here, change as desired)

    A. Create the directory:
    C:\app\browser\firefox\openwithgvim

    B. Place the following files inside it:
    a. manifest.json
    b. background.js
    c. content.js
    d. gvim_host.json
    e. gvim_host.bat
    f. gvim_host.py
    g. install_host.reg

    3. Installing the Native Host

    A. Double-click install_host.reg to register the native host.
    B. Ensure Python is on your PATH.
    C. Restart Firefox after making changes.

    4. Testing the Extension

    A. Load the extension as a temporary add-on in Firefox.
    B. This adds a new Firefox context menu "Open page source in gVim".
    B. Right-click any page & choose "Open page source in gVim".
    C. Observe the logs in the Browser Console.
    D. Observe the debug logs in host_debug.log & host_stderr.log.
    E. Confirm that:
    a. The temp file is created.
    b. The native host returns success.
    c. The editor launch command executes.

    F. On Windows, the editor window did not appear but there
    were no errors in the debug logs. This is the open question.

    5. The Open Question for Windows and Linux Users

    A. Why do GUI applications launched from a Firefox native messaging
    host not appear on the user's desktop in Windows?

    Do they appear on other operating systems?

    B. Observations:
    a. The editor launch command executes successfully.
    b. No error is thrown.
    c. The temp file is created.
    d. The native host returns success.
    e. No editor process appears in tasklist.
    f. The same editor launches normally when invoked by
    Firefox's built-in external editor mechanism.

    C. Hypothesis:
    a. Firefox native hosts run in a restricted background session.
    b. Windows prevents GUI applications from appearing in the interactive
    desktop session when launched from that context.

    D. Invitation:
    a. Linux users may be able to run the same code successfully.
    b. Windows experts may know a workaround that does not require
    writing a helper executable.

    6. Files (each indented one space for Usenet safety)

    manifest.json
    {
    "manifest_version": 2,
    "name": "Open Page Source in gVim",
    "version": "1.0",
    "description": "Right-click a page to open its HTML source in gVim.",
    "permissions": [
    "contextMenus",
    "tabs",
    "nativeMessaging",
    "activeTab",
    "<all_urls>"
    ],
    "background": {
    "scripts": ["background.js"],
    "persistent": true
    },
    "content_scripts": [
    {
    "matches": ["<all_urls>"],
    "js": ["content.js"],
    "run_at": "document_end"
    }
    ],
    "browser_specific_settings": {
    "gecko": {
    "id": "gvim-opener@example"
    }
    }
    }

    background.js
    console.log("BACKGROUND SCRIPT LOADED");

    browser.runtime.onInstalled.addListener(() => {
    console.log("onInstalled fired: creating context menu");
    browser.contextMenus.create({
    id: "open-page-source-gvim",
    title: "Open page source in gVim",
    contexts: ["page"]
    });
    });

    browser.contextMenus.onClicked.addListener(async (info, tab) => {
    console.log("CONTEXT MENU CLICKED", info, tab);

    if (info.menuItemId !== "open-page-source-gvim") {
    return;
    }

    try {
    const html = await browser.tabs.sendMessage(tab.id, { type: "GET_HTML" });
    if (!html) return;

    const payload = { url: tab.url, html: html };

    browser.runtime.sendNativeMessage("gvim_opener", payload)
    .then(response => console.log("Native host response:", response))
    .catch(err => console.error("Error from native host:", err));

    } catch (err) {
    console.error("Error communicating with content script:", err);
    }
    });

    content.js
    console.log("CONTENT SCRIPT LOADED on", window.location.href);

    browser.runtime.onMessage.addListener((msg, sender) => {
    if (msg && msg.type === "GET_HTML") {
    try {
    const html = document.documentElement.outerHTML;
    return Promise.resolve(html);
    } catch (e) {
    return Promise.resolve(null);
    }
    }
    });

    gvim_host.json
    {
    "name": "gvim_opener",
    "description": "Open page source in gVim",
    "path": "C:\\app\\browser\\firefox\\openwithgvim\\gvim_host.bat",
    "type": "stdio",
    "allowed_extensions": [
    "gvim-opener@example"
    ]
    }

    install_host.reg
    Windows Registry Editor Version 5.00

    [HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\gvim_opener]
    @="C:\\app\\browser\\firefox\\openwithgvim\\gvim_host.json"

    gvim_host.bat
    @echo off
    echo gvim_host.bat starting >> "C:\app\browser\firefox\openwithgvim\host_debug.log"
    python "C:\app\browser\firefox\openwithgvim\gvim_host.py" 2>> "C:\app\browser\firefox\openwithgvim\host_stderr.log"
    echo gvim_host.bat finished >> "C:\app\browser\firefox\openwithgvim\host_debug.log"

    gvim_host.py
    import sys
    import struct
    import json
    import subprocess
    import tempfile
    import os

    EDITOR_PATH = r"C:\app\editor\txt\notepad++\notepad++.exe"
    # EDITOR_PATH = r"C:\app\editor\txt\vim\vim82\gvim.exe"

    def log(msg):
    sys.stderr.write(str(msg) + "\n")
    sys.stderr.flush()

    def read_message():
    raw_length = sys.stdin.buffer.read(4)
    if not raw_length:
    log("No length header, stdin closed")
    return None
    length = struct.unpack("<I", raw_length)[0]
    data = sys.stdin.buffer.read(length).decode("utf-8")
    return json.loads(data)

    def send_message(msg):
    encoded = json.dumps(msg).encode("utf-8")
    sys.stdout.buffer.write(struct.pack("<I", len(encoded)))
    sys.stdout.buffer.write(encoded)
    sys.stdout.buffer.flush()

    def main():
    while True:
    msg = read_message()
    if msg is None:
    break

    html = msg.get("html", "")
    url = msg.get("url", "")

    fd, path = tempfile.mkstemp(suffix=".html", prefix="gvim_")
    os.close(fd)
    with open(path, "w", encoding="utf-8") as f:
    f.write(html)

    try:
    subprocess.Popen(
    ["cmd.exe", "/c", "start", "", EDITOR_PATH, path],
    shell=False
    )
    send_message({"ok": True, "file": path, "url": url})
    except Exception as e:
    send_message({"ok": False, "error": str(e)})

    if __name__ == "__main__":
    main()

    Tested only on Windows 10, Firefox 133.0 (64-bit) using my file specs.
    --
    I'm never afraid to say what I don't know or what I do know as my ego isn't tied to pretending otherwise, unlike most people, who are always afraid.
    --- Synchronet 3.21b-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to alt.comp.software.firefox,alt.comp.os.windows-10,alt.os.linux on Tue Feb 24 14:42:38 2026
    From Newsgroup: alt.os.linux

    Maria Sophia wrote:
    This tutorial can be rightly titled something like:
    *Adding a right-click Firefox menu item that calls a native program*

    UPDATE:

    I hate when we can't wrestle control of any app to make it do our bidding. But... each failure at the 99.9% finish line, teaches us something that
    we wouldn't have known unless someone told us or we learned the hard way.

    This is learning how Firefox works in Windows the hard way for sure.
    I tried again today, but with two changes:
    a. Notepad.exe
    b. Temp file

    Everything is working except the final step, which should work in Linux,
    but which isn't working (yet) in Windows. Yesterday I switched from gVim to Notepad++, and today I switched to Notepad.exe on the temporary file.

    Same thing happened again.

    In all cases, Notepad++, gVim and even plain old Windows Notepad all fail
    to appear for the same reason, which proves the issue has nothing to do
    with the editor choice, the command line, Python, the BAT wrapper, the
    registry entry, or the Firefox extension itself.

    What this demonstrates is that Firefox's native messaging host on Windows
    runs inside a non-interactive desktop session, which appears to be one that cannot display GUI windows of any kind (as far as I can tell by testing).

    The entire pipeline works perfectly, the temp file is created, the native
    host returns success, and every step is logged, but Windows simply will not show GUI applications launched from that isolated session.

    This follow-up therefore focuses on a test of the native "Notepad.exe"
    which failed at the last step, just as the other editors did.

    The only change from yesterday's files are the gvim_host.py has a minor
    change so as to invoke notepad.exe on the *temporary* file FF created.

    gvim_host.py
    import sys
    import struct
    import json
    import subprocess
    import tempfile
    import os

    EDITOR_PATH = r"C:\Windows\System32\notepad.exe"

    def log(msg):
    sys.stderr.write(str(msg) + "\n")
    sys.stderr.flush()

    def read_message():
    raw_length = sys.stdin.buffer.read(4)
    if not raw_length:
    log("No length header, stdin closed")
    return None
    length = struct.unpack("<I", raw_length)[0]
    data = sys.stdin.buffer.read(length).decode("utf-8")
    return json.loads(data)

    def send_message(msg):
    encoded = json.dumps(msg).encode("utf-8")
    sys.stdout.buffer.write(struct.pack("<I", len(encoded)))
    sys.stdout.buffer.write(encoded)
    sys.stdout.buffer.flush()

    def main():
    while True:
    msg = read_message()
    if msg is None:
    break

    html = msg.get("html", "")
    url = msg.get("url", "")

    fd, path = tempfile.mkstemp(suffix=".html", prefix="gvim_")
    os.close(fd)
    with open(path, "w", encoding="utf-8") as f:
    f.write(html)

    try:
    subprocess.Popen([EDITOR_PATH, path], shell=False)
    send_message({"ok": True, "file": path, "url": url})
    except Exception as e:
    send_message({"ok": False, "error": str(e)})

    if __name__ == "__main__":
    main()

    In summary, this related follow-up test uses the exact same extension code
    and almost the same native host code, but instead of trying to launch the
    HTML source in a full-featured editor, it attempts the simplest GUI program that ships with Windows, namely plain old Notepad.exe.

    The result is important precisely because it fails in exactly the same way.

    The temp file is created, the native host runs, the HTML is written, the command is executed, log files are written, and the host returns success,
    yet the Notepad window never appears.

    This identical failure across gVim, Notepad++, and now Notepad itself
    proves that the editor choice was never the issue. The real problem is that Firefox's native messaging host on Windows runs inside a non-interactive desktop session that apparently cannot display GUI windows of any kind.

    This follow-up therefore documents a second, simpler, more controlled
    failure that reinforces the conclusion that Windows session isolation, and
    not the extension, not Python, not the BAT file, and not the editor, is preventing any GUI program from appearing. At least as far as I can tell.

    Tested only on Windows 10, Firefox 133.0 (64-bit) using my file specs.
    --
    I volunteer to help people today & they volunteer to help me tomorrow
    --- Synchronet 3.21b-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to alt.comp.software.firefox,alt.comp.os.windows-10,alt.os.linux on Tue Feb 24 14:57:34 2026
    From Newsgroup: alt.os.linux

    Maria Sophia wrote:
    Everything is working except the final step, which should work in Linux,
    but which isn't working (yet) in Windows. Yesterday I switched from gVim to Notepad++, and today I switched to Notepad.exe on the temporary file.

    The original dream of writing an extension allowing right-clicking in
    Firefox to open page source in an editor should work on Linux exactly as we imagined it. But not on Windows.

    As far as I've been able to ascertain by testing, no GUI program will ever appear on a Windows desktop when launched from a Firefox native host on Windows, but, it should still work on Linux.

    There's just no magic escape hatch on Windows because even if we call a
    batch file, on Windows it inherits the exact same session, the exact same desktop, the exact same window station and the exact same restrictions as
    the Python process that launched it because Windows forbids GUI windows in
    that session.

    Linux desktop sessions are not isolated the way Windows sessions are.
    When Firefox launches a native host:
    a. it inherits our $DISPLAY
    b. it inherits our $XAUTHORITY
    c. it inherits our Wayland session
    d. it inherits our environment variables
    e. it inherits our user permissions
    So the host process is literally running inside our desktop session.

    Windows, by contrast, launches the native host in a background, invisible, non-interactive session with:
    a. no desktop
    b. no window station
    c. no GUI access
    d. no ability to display windows

    This is why Notepad, Notepad++, and gVim all fail on Windows, yet the
    $EDITOR should work on Linux using almost the same code provided.

    Sigh.
    Lesson learned... The hard way.

    As Lawrence intoned, there's a reason hackers aren't on Windows...
    That's fer' sure.

    But it's no over until the fat lady sings... as no app on any operating
    system is more powerful than we are in wresting control over it.
    --
    Every Usenet post should strive to add palpable additional value
    so that we can all delight in dissemination of useful knowledge.
    --- Synchronet 3.21b-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to alt.comp.software.firefox,alt.comp.os.windows-10,alt.os.linux on Tue Feb 24 16:51:23 2026
    From Newsgroup: alt.os.linux

    Maria Sophia wrote:
    As far as I've been able to ascertain by testing, no GUI program will ever appear on a Windows desktop when launched from a Firefox native host on Windows, but, it should still work on Linux.

    Voila!
    Success at last!

    The native host runs inside a restricted, non-interactive Windows session. Unfortunately, we've learned that Windows-GUI programs apparently cannot appear from that session, but file I/O works perfectly.

    So the example file-based-change-detector below is a natural fit.

    Once I stopped fighting that one immovable Windows-GUI wall, a whole
    landscape of genuinely useful, practical, even elegant but non-GUI possibilities open up for our first working Firefox extension.

    Let's write a simple webpage-change detector instead.
    It will tell us if a web page has Changed or NotChanged.

    Overview:
    a. We'll keep everything in the Firefox home directory, as before
    C:\app\browser\firefox\openwithgvim\*.{js,bat,json,py,log,reg}
    b. And create a subdirectory to watch if web pages have been changed.
    C:\app\browser\firefox\openwithgvim\pagewatch\{report.txt,last.html}
    c. Inside pagewatch, the Python host will maintain:
    i. last.html, the last version of the page
    ii. report.txt, the human-readable "changed / not changed" result
    iii. (optional) snapshots/, if we ever want to archive versions
    This keeps everything tidy and avoids clutter

    The logic will be simple because this is to be an example extension.
    A. If last.html does not exist:
    Write the new HTML to last.html
    Write a report saying: FIRST RUN - baseline saved
    Return { ok: true, changed: true }
    B. If last.html does exist:
    Read it
    Compare it to the new HTML (simple string comparison)
    C. If identical:
    Write: NO CHANGE
    Return { ok: true, changed: false }
    D. If different:
    Write:
    CHANGED
    Old length: ####
    New length: ####
    Timestamp: ...
    Overwrite last.html with the new HTML
    Return { ok: true, changed: true }
    E. In testing, I had to add code to overcome file locks gracefully.

    Here is every step of the test procedure:
    1. Start Firefox
    2. Click your bookmark for about:debugging#/runtime/this-firefox
    3. Click "Load Temporary Add-on..."
    4. Select C:\app\browser\firefox\openwithgvim\manifest.json
    5. Open a local file file:///C:/data/amazon/vine/vine.htm
    6. Rightclick in white space on that local file
    7. Select "Open page source in gVim" (we never changed it)
    8. Check the pagewatch report file for status
    C:\> type C:\app\browser\firefox\openwithgvim\pagewatch\report.txt
    NO CHANGE
    URL: file:///C:/data/amazon/vine/vine.htm
    Timestamp: 2026-02-24 16:34:02
    Length: 53565 bytes
    9. Edit the source (Control+U or rightclick > View page source
    Change something & refresh the page
    Note that ViewPageSource is an edit due to these settings:
    about:config > view_source.editor
    view_source.editor.external = true
    view_source.editor.path = C:\app\editor\txt\vi\gvim.exe
    10. Check the pagewatch report file for status
    C:\> type C:\app\browser\firefox\openwithgvim\pagewatch\report.txt
    CHANGED
    URL: file:///C:/data/sys/apppath/vistuff/vine.htm
    Timestamp: 2026-02-24 16:39:30
    Old length: 53565 bytes
    New length: 53541 bytes

    The only file that changed was the python native messaging host script.
    =====< cut below for gvim_host.py >=====
    import sys
    import struct
    import json
    import os
    from datetime import datetime

    # Base directory for everything
    BASE_DIR = r"C:\app\browser\firefox\openwithgvim"
    WATCH_DIR = os.path.join(BASE_DIR, "pagewatch")

    LAST_FILE = os.path.join(WATCH_DIR, "last.html")
    REPORT_FILE = os.path.join(WATCH_DIR, "report.txt")

    DEBUG_LOG = os.path.join(BASE_DIR, "host_debug.log")
    ERR_LOG = os.path.join(BASE_DIR, "host_stderr.log")


    def log(msg):
    """Write debug messages to stderr log, but never crash if the file is locked."""
    try:
    with open(ERR_LOG, "a", encoding="utf-8") as f:
    f.write(str(msg) + "\n")
    except Exception:
    # Windows sometimes locks files; logging must never kill the host
    pass


    def ensure_directories():
    """Create the pagewatch directory if missing."""
    if not os.path.exists(WATCH_DIR):
    os.makedirs(WATCH_DIR, exist_ok=True)


    def read_message():
    """Read a native message from Firefox."""
    raw_length = sys.stdin.buffer.read(4)
    if not raw_length:
    log("No length header, stdin closed")
    return None

    length = struct.unpack("<I", raw_length)[0]
    data = sys.stdin.buffer.read(length).decode("utf-8")
    return json.loads(data)


    def send_message(msg):
    """Send a native message back to Firefox."""
    encoded = json.dumps(msg).encode("utf-8")
    sys.stdout.buffer.write(struct.pack("<I", len(encoded)))
    sys.stdout.buffer.write(encoded)
    sys.stdout.buffer.flush()


    def write_report(text):
    """Write a human-readable report."""
    try:
    with open(REPORT_FILE, "w", encoding="utf-8") as f:
    f.write(text)
    except Exception as e:
    log(f"Failed to write report: {e}")


    def main():
    ensure_directories()

    while True:
    msg = read_message()
    if msg is None:
    break

    html = msg.get("html", "")
    url = msg.get("url", "")

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    # FIRST RUN - no last.html exists
    if not os.path.exists(LAST_FILE):
    try:
    with open(LAST_FILE, "w", encoding="utf-8") as f:
    f.write(html)
    except Exception as e:
    log(f"Failed to write baseline last.html: {e}")

    report = (
    f"FIRST RUN - baseline saved\n"
    f"URL: {url}\n"
    f"Timestamp: {timestamp}\n"
    f"Length: {len(html)} bytes\n"
    )
    write_report(report)

    send_message({"ok": True, "changed": True, "first_run": True})
    continue

    # SUBSEQUENT RUNS - compare with last.html
    try:
    with open(LAST_FILE, "r", encoding="utf-8") as f:
    old_html = f.read()
    except Exception as e:
    log(f"Error reading last.html: {e}")
    old_html = ""

    changed = (html != old_html)

    if not changed:
    report = (
    f"NO CHANGE\n"
    f"URL: {url}\n"
    f"Timestamp: {timestamp}\n"
    f"Length: {len(html)} bytes\n"
    )
    write_report(report)

    send_message({"ok": True, "changed": False})
    continue

    # If changed, overwrite last.html
    try:
    with open(LAST_FILE, "w", encoding="utf-8") as f:
    f.write(html)
    except Exception as e:
    log(f"Failed to update last.html: {e}")

    report = (
    f"CHANGED\n"
    f"URL: {url}\n"
    f"Timestamp: {timestamp}\n"
    f"Old length: {len(old_html)} bytes\n"
    f"New length: {len(html)} bytes\n"
    )
    write_report(report)

    send_message({"ok": True, "changed": True})


    if __name__ == "__main__":
    main()
    =====< cut above for gvim_host.py >=====

    We never lose when customizing Firefox but sometimes it's not easy.
    Tested only on Windows 10, Firefox 133.0 (64-bit) using my file specs.
    --
    Every person on this newsgroup adds their own flavor of value to the team.
    --- Synchronet 3.21b-Linux NewsLink 1.2
  • From Hank Rogers@Hank@nospam.invalid to alt.comp.software.firefox,alt.comp.os.windows-10,alt.os.linux on Tue Feb 24 20:11:21 2026
    From Newsgroup: alt.os.linux

    Maria Sophia wrote on 2/24/2026 4:51 PM:
    Maria Sophia wrote:
    As far as I've been able to ascertain by testing, no GUI program will
    ever
    appear on a Windows desktop when launched from a Firefox native host on
    Windows, but, it should still work on Linux.

    Voila!
    Success at last!

    The native host runs inside a restricted, non-interactive Windows session. Unfortunately, we've learned that Windows-GUI programs apparently cannot appear from that session, but file I/O works perfectly.

    So the example file-based-change-detector below is a natural fit.

    Once I stopped fighting that one immovable Windows-GUI wall, a whole landscape of genuinely useful, practical, even elegant but non-GUI possibilities open up for our first working Firefox extension.
    Let's write a simple webpage-change detector instead. It will tell us if
    a web page has Changed or NotChanged.

    Overview:
    a. We'll keep everything in the Firefox home directory, as before
    -a-a C:\app\browser\firefox\openwithgvim\*.{js,bat,json,py,log,reg}
    b. And create a subdirectory to watch if web pages have been changed.
    -a-a C:\app\browser\firefox\openwithgvim\pagewatch\{report.txt,last.html}
    c. Inside pagewatch, the Python host will maintain:
    -a-a i. last.html, the last version of the page
    -a-a ii. report.txt, the human-readable "changed / not changed" result
    -a-a iii. (optional) snapshots/, if we ever want to archive versions
    This keeps everything tidy and avoids clutter

    The logic will be simple because this is to be an example extension.
    A. If last.html does not exist:
    -a-a Write the new HTML to last.html
    -a-a Write a report saying: FIRST RUN - baseline saved
    -a-a Return { ok: true, changed: true }
    B. If last.html does exist:
    -a-a Read it
    -a-a Compare it to the new HTML (simple string comparison)
    C. If identical:
    -a-a Write: NO CHANGE
    -a-a Return { ok: true, changed: false }
    D. If different:
    -a-a Write:
    -a-a-a CHANGED
    -a-a-a Old length: ####
    -a-a-a New length: ####
    -a-a-a Timestamp: ...
    -a-a Overwrite last.html with the new HTML
    -a-a Return { ok: true, changed: true }
    E. In testing, I had to add code to overcome file locks gracefully.

    Here is every step of the test procedure:
    1. Start Firefox
    2. Click your bookmark for about:debugging#/runtime/this-firefox
    3. Click "Load Temporary Add-on..."
    4. Select C:\app\browser\firefox\openwithgvim\manifest.json
    5. Open a local file file:///C:/data/amazon/vine/vine.htm
    6. Rightclick in white space on that local file
    7. Select "Open page source in gVim" (we never changed it)
    8. Check the pagewatch report file for status
    -a C:\> type C:\app\browser\firefox\openwithgvim\pagewatch\report.txt
    -a-a-a-a-a-a NO CHANGE
    -a-a-a-a-a-a URL: file:///C:/data/amazon/vine/vine.htm
    -a-a-a-a-a-a Timestamp: 2026-02-24 16:34:02
    -a-a-a-a-a-a Length: 53565 bytes
    9. Edit the source (Control+U or rightclick > View page source
    -a Change something & refresh the page -a Note that ViewPageSource is an edit due to these settings:
    -a-a about:config > view_source.editor
    -a-a view_source.editor.external = true
    -a-a view_source.editor.path = C:\app\editor\txt\vi\gvim.exe
    10. Check the pagewatch report file for status
    -a-a C:\> type C:\app\browser\firefox\openwithgvim\pagewatch\report.txt
    -a-a-a-a-a-a-a CHANGED
    -a-a-a-a-a-a-a URL: file:///C:/data/sys/apppath/vistuff/vine.htm
    -a-a-a-a-a-a-a Timestamp: 2026-02-24 16:39:30
    -a-a-a-a-a-a-a Old length: 53565 bytes
    -a-a-a-a-a-a-a New length: 53541 bytes

    The only file that changed was the python native messaging host script. =====< cut below for gvim_host.py >=====
    import sys
    import struct
    import json
    import os
    from datetime import datetime

    # Base directory for everything
    BASE_DIR = r"C:\app\browser\firefox\openwithgvim"
    WATCH_DIR = os.path.join(BASE_DIR, "pagewatch")

    LAST_FILE = os.path.join(WATCH_DIR, "last.html")
    REPORT_FILE = os.path.join(WATCH_DIR, "report.txt")

    DEBUG_LOG = os.path.join(BASE_DIR, "host_debug.log")
    ERR_LOG = os.path.join(BASE_DIR, "host_stderr.log")


    def log(msg):
    -a-a-a """Write debug messages to stderr log, but never crash if the file is locked."""
    -a-a-a try:
    -a-a-a-a-a-a-a with open(ERR_LOG, "a", encoding="utf-8") as f:
    -a-a-a-a-a-a-a-a-a-a-a f.write(str(msg) + "\n")
    -a-a-a except Exception:
    -a-a-a-a-a-a-a # Windows sometimes locks files; logging must never kill the host
    -a-a-a-a-a-a-a pass


    def ensure_directories():
    -a-a-a """Create the pagewatch directory if missing."""
    -a-a-a if not os.path.exists(WATCH_DIR):
    -a-a-a-a-a-a-a os.makedirs(WATCH_DIR, exist_ok=True)


    def read_message():
    -a-a-a """Read a native message from Firefox."""
    -a-a-a raw_length = sys.stdin.buffer.read(4)
    -a-a-a if not raw_length:
    -a-a-a-a-a-a-a log("No length header, stdin closed")
    -a-a-a-a-a-a-a return None

    -a-a-a length = struct.unpack("<I", raw_length)[0]
    -a-a-a data = sys.stdin.buffer.read(length).decode("utf-8")
    -a-a-a return json.loads(data)


    def send_message(msg):
    -a-a-a """Send a native message back to Firefox."""
    -a-a-a encoded = json.dumps(msg).encode("utf-8")
    -a-a-a sys.stdout.buffer.write(struct.pack("<I", len(encoded)))
    -a-a-a sys.stdout.buffer.write(encoded)
    -a-a-a sys.stdout.buffer.flush()


    def write_report(text):
    -a-a-a """Write a human-readable report."""
    -a-a-a try:
    -a-a-a-a-a-a-a with open(REPORT_FILE, "w", encoding="utf-8") as f:
    -a-a-a-a-a-a-a-a-a-a-a f.write(text)
    -a-a-a except Exception as e:
    -a-a-a-a-a-a-a log(f"Failed to write report: {e}")


    def main():
    -a-a-a ensure_directories()

    -a-a-a while True:
    -a-a-a-a-a-a-a msg = read_message()
    -a-a-a-a-a-a-a if msg is None:
    -a-a-a-a-a-a-a-a-a-a-a break

    -a-a-a-a-a-a-a html = msg.get("html", "")
    -a-a-a-a-a-a-a url = msg.get("url", "")

    -a-a-a-a-a-a-a timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    -a-a-a-a-a-a-a # FIRST RUN - no last.html exists
    -a-a-a-a-a-a-a if not os.path.exists(LAST_FILE):
    -a-a-a-a-a-a-a-a-a-a-a try:
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a with open(LAST_FILE, "w", encoding="utf-8") as f:
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a-a f.write(html)
    -a-a-a-a-a-a-a-a-a-a-a except Exception as e:
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a log(f"Failed to write baseline last.html: {e}")

    -a-a-a-a-a-a-a-a-a-a-a report = (
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a f"FIRST RUN - baseline saved\n"
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a f"URL: {url}\n"
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a f"Timestamp: {timestamp}\n"
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a f"Length: {len(html)} bytes\n"
    -a-a-a-a-a-a-a-a-a-a-a )
    -a-a-a-a-a-a-a-a-a-a-a write_report(report)

    -a-a-a-a-a-a-a-a-a-a-a send_message({"ok": True, "changed": True, "first_run": True})
    -a-a-a-a-a-a-a-a-a-a-a continue

    -a-a-a-a-a-a-a # SUBSEQUENT RUNS - compare with last.html
    -a-a-a-a-a-a-a try:
    -a-a-a-a-a-a-a-a-a-a-a with open(LAST_FILE, "r", encoding="utf-8") as f:
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a old_html = f.read()
    -a-a-a-a-a-a-a except Exception as e:
    -a-a-a-a-a-a-a-a-a-a-a log(f"Error reading last.html: {e}")
    -a-a-a-a-a-a-a-a-a-a-a old_html = ""

    -a-a-a-a-a-a-a changed = (html != old_html)

    -a-a-a-a-a-a-a if not changed:
    -a-a-a-a-a-a-a-a-a-a-a report = (
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a f"NO CHANGE\n"
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a f"URL: {url}\n"
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a f"Timestamp: {timestamp}\n"
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a f"Length: {len(html)} bytes\n"
    -a-a-a-a-a-a-a-a-a-a-a )
    -a-a-a-a-a-a-a-a-a-a-a write_report(report)

    -a-a-a-a-a-a-a-a-a-a-a send_message({"ok": True, "changed": False})
    -a-a-a-a-a-a-a-a-a-a-a continue

    -a-a-a-a-a-a-a # If changed, overwrite last.html
    -a-a-a-a-a-a-a try:
    -a-a-a-a-a-a-a-a-a-a-a with open(LAST_FILE, "w", encoding="utf-8") as f:
    -a-a-a-a-a-a-a-a-a-a-a-a-a-a-a f.write(html)
    -a-a-a-a-a-a-a except Exception as e:
    -a-a-a-a-a-a-a-a-a-a-a log(f"Failed to update last.html: {e}")

    -a-a-a-a-a-a-a report = (
    -a-a-a-a-a-a-a-a-a-a-a f"CHANGED\n"
    -a-a-a-a-a-a-a-a-a-a-a f"URL: {url}\n"
    -a-a-a-a-a-a-a-a-a-a-a f"Timestamp: {timestamp}\n"
    -a-a-a-a-a-a-a-a-a-a-a f"Old length: {len(old_html)} bytes\n"
    -a-a-a-a-a-a-a-a-a-a-a f"New length: {len(html)} bytes\n"
    -a-a-a-a-a-a-a )
    -a-a-a-a-a-a-a write_report(report)

    -a-a-a-a-a-a-a send_message({"ok": True, "changed": True})


    if __name__ == "__main__":
    -a-a-a main()
    =====< cut above for gvim_host.py >=====

    We never lose when customizing Firefox but sometimes it's not easy.
    Tested only on Windows 10, Firefox 133.0 (64-bit) using my file specs.

    I would like to see some tutorials on Seamonkey. Especially the later versions.

    --- Synchronet 3.21b-Linux NewsLink 1.2
  • From Maria Sophia@mariasophia@comprehension.com to alt.comp.software.firefox,alt.comp.os.windows-10,alt.os.linux on Wed Feb 25 07:15:54 2026
    From Newsgroup: alt.os.linux

    MAJOR UPDATE:

    Since this started as a Firefox extension to call a Linux/Windows GUI
    but it morphed (by necessity, at least on Windows) to a non-GUI action,
    here is the completed package so that others can test it themselves.

    (Linux instructions are appended at the bottom of this tutorial.)

    A. Create the extension directory and copy all source files into it
    C:\> mkdir C:\app\browser\firefox\pagewatch\

    B. Install the native messaging host and verify it
    C:\> install_host.reg

    Verify that the registry entry was created:
    C:\> reg query HKCU\Software\Mozilla\NativeMessagingHosts\pagewatch_host

    If successful, Windows will print the path to:
    C:\app\browser\firefox\pagewatch\pagewatch_host.json

    C. Start Firefox and load the extension

    a. Open Firefox
    b. Navigate to:
    about:debugging#/runtime/this-firefox
    c. Click "Load Temporary Add-on..."
    d. Select:
    C:\app\browser\firefox\pagewatch\manifest.json
    e. Open a local file, for example:
    file:///C:/data/amazon/vine/vine.htm
    f. Rightclick in whitespace on the page
    g. Select the menu item:
    Detect webpage changes
    h. Check the report file:
    C:\> type C:\app\browser\firefox\pagewatch\log\report.txt

    Example output on first run:
    FIRST RUN - baseline saved
    URL: file:///C:/data/amazon/vine/vine.htm
    Timestamp: 2026-02-24 16:34:02
    Length: 53565 bytes

    i. Edit the page source (Ctrl+U or View Page Source), make a change,
    save, refresh.

    Note: View Page Source is editable because of:
    about:config
    view_source.editor.external = true
    view_source.editor.path = C:\app\editor\txt\vi\gvim.exe

    j. Run "Detect webpage changes" again
    k. Check the report file again:

    CHANGED
    URL: file:///C:/data/amazon/vine/vine.htm
    Timestamp: 2026-02-24 16:39:30
    Old length: 53565 bytes
    New length: 53541 bytes


    =====< below is manifest.json >=====
    {
    "manifest_version": 3,
    "name": "PageWatch Webpage Change Detector",
    "version": "1.0",

    "description": "Detects whether a webpage has changed using a native
    Python host.",

    "permissions": [
    "contextMenus",
    "nativeMessaging",
    "scripting",
    "tabs"
    ],

    "background": {
    "service_worker": "background.js"
    },

    "content_scripts": [
    {
    "matches": ["<all_urls>"],
    "js": ["content.js"]
    }
    ]
    }
    =====< above is manifest.json >=====


    =====< below is background.js >=====
    console.log("BACKGROUND SCRIPT LOADED");

    browser.runtime.onInstalled.addListener(() => {
    console.log("onInstalled fired: creating context menu");
    browser.contextMenus.create({
    id: "detect-web-page-changes",
    title: "Detect webpage changes",
    contexts: ["page"]
    });
    });

    browser.contextMenus.onClicked.addListener(async (info, tab) => {
    console.log("CONTEXT MENU CLICKED", info, tab);

    if (info.menuItemId !== "detect-web-page-changes") {
    console.log("Menu ID mismatch, ignoring click");
    return;
    }

    try {
    console.log("Sending message to content script: GET_HTML");
    const html = await browser.tabs.sendMessage(tab.id, { type: "GET_HTML"
    });
    console.log("Received response from content script. HTML length:",
    html ? html.length : "null");

    if (!html) {
    console.error("Failed to retrieve page HTML from content script");
    return;
    }

    const payload = {
    url: tab.url,
    html: html
    };

    console.log("Sending native message to host pagewatch_host with
    payload:", {
    url: payload.url,
    html_length: payload.html.length
    });

    browser.runtime.sendNativeMessage("pagewatch_host", payload)
    .then(response => {
    console.log("Native host response:", response);
    })
    .catch(err => {
    console.error("Error from native host:", err);
    });

    } catch (err) {
    console.error("Error communicating with content script:", err);
    }
    });
    =====< above is background.js >=====


    =====< below is content.js >=====
    browser.runtime.onMessage.addListener((msg) => {
    if (msg.type === "GET_HTML") {
    return Promise.resolve(document.documentElement.outerHTML);
    }
    });
    =====< above is content.js >=====


    =====< below is pagewatch_host.bat >=====
    @echo off
    python "C:\app\browser\firefox\pagewatch\pagewatch_host.py"
    =====< above is pagewatch_host.bat >=====


    =====< below is pagewatch_host.json >=====
    {
    "name": "pagewatch_host",
    "description": "Native messaging host for PageWatch webpage change detector",
    "path": "C:\\app\\browser\\firefox\\pagewatch\\pagewatch_host.bat",
    "type": "stdio",
    "allowed_extensions": [ "pagewatch@example.com" ]
    }
    =====< above is pagewatch_host.json >=====


    =====< below is install_host.reg >=====
    Windows Registry Editor Version 5.00

    [HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\pagewatch_host]
    @="C:\\app\\browser\\firefox\\pagewatch\\pagewatch_host.json"
    =====< above is install_host.reg >=====


    =====< below is pagewatch_host.py >=====
    import sys
    import struct
    import json
    import os
    from datetime import datetime

    BASE_DIR = r"C:\app\browser\firefox\pagewatch"
    LOG_DIR = os.path.join(BASE_DIR, "log")

    LAST_FILE = os.path.join(LOG_DIR, "last.html")
    REPORT_FILE = os.path.join(LOG_DIR, "report.txt")

    DEBUG_LOG = os.path.join(LOG_DIR, "host_debug.log")
    ERR_LOG = os.path.join(LOG_DIR, "host_stderr.log")


    def log(msg):
    try:
    with open(ERR_LOG, "a", encoding="utf-8") as f:
    f.write(str(msg) + "\n")
    except Exception:
    pass


    def ensure_directories():
    os.makedirs(LOG_DIR, exist_ok=True)


    def read_message():
    raw_length = sys.stdin.buffer.read(4)
    if not raw_length:
    log("No length header, stdin closed")
    return None

    length = struct.unpack("<I", raw_length)[0]
    data = sys.stdin.buffer.read(length).decode("utf-8")
    return json.loads(data)


    def send_message(msg):
    encoded = json.dumps(msg).encode("utf-8")
    sys.stdout.buffer.write(struct.pack("<I", len(encoded)))
    sys.stdout.buffer.write(encoded)
    sys.stdout.buffer.flush()


    def write_report(text):
    try:
    with open(REPORT_FILE, "w", encoding="utf-8") as f:
    f.write(text)
    except Exception as e:
    log(f"Failed to write report: {e}")


    def main():
    ensure_directories()

    while True:
    msg = read_message()
    if msg is None:
    break

    html = msg.get("html", "")
    url = msg.get("url", "")

    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    if not os.path.exists(LAST_FILE):
    try:
    with open(LAST_FILE, "w", encoding="utf-8") as f:
    f.write(html)
    except Exception as e:
    log(f"Failed to write baseline last.html: {e}")

    report = (
    f"FIRST RUN - baseline saved\n"
    f"URL: {url}\n"
    f"Timestamp: {timestamp}\n"
    f"Length: {len(html)} bytes\n"
    )
    write_report(report)

    send_message({"ok": True, "changed": True, "first_run": True})
    continue

    try:
    with open(LAST_FILE, "r", encoding="utf-8") as f:
    old_html = f.read()
    except Exception as e:
    log(f"Error reading last.html: {e}")
    old_html = ""

    changed = (html != old_html)

    if not changed:
    report = (
    f"NO CHANGE\n"
    f"URL: {url}\n"
    f"Timestamp: {timestamp}\n"
    f"Length: {len(html)} bytes\n"
    )
    write_report(report)

    send_message({"ok": True, "changed": False})
    continue

    try:
    with open(LAST_FILE, "w", encoding="utf-8") as f:
    f.write(html)
    except Exception as e:
    log(f"Failed to update last.html: {e}")

    report = (
    f"CHANGED\n"
    f"URL: {url}\n"
    f"Timestamp: {timestamp}\n"
    f"Old length: {len(old_html)} bytes\n"
    f"New length: {len(html)} bytes\n"
    )
    write_report(report)

    send_message({"ok": True, "changed": True})


    if __name__ == "__main__":
    main()
    =====< above is pagewatch_host.py >=====

    Below are just the changes required to test PageWatch on Linux.
    Changes required to run PageWatch on Linux
    a. Replace the Windows batch file with a Linux shell script
    1. Create this file:
    /home/you/browser/firefox/pagewatch/pagewatch_host.sh
    2. Contents:
    #!/bin/sh
    python3 "/home/you/browser/firefox/pagewatch/pagewatch_host.py"
    3. Make it executable:
    chmod +x /home/you/browser/firefox/pagewatch/pagewatch_host.sh
    b. Update the native messaging host JSON file
    1. Move it to:
    ~/.mozilla/native-messaging-hosts/pagewatch_host.json
    2. Update the "path" entry to point to the Linux shell script:
    {
    "name": "pagewatch_host",
    "description": "Native messaging host for PageWatch webpage change detector",
    "path": "/home/you/browser/firefox/pagewatch/pagewatch_host.sh",
    "type": "stdio",
    "allowed_extensions": [ "pagewatch@example.com" ]
    }
    c. Remove the Windows registry step
    1. Linux does not use the registry.
    2. Install the host by copying the JSON file:
    mkdir -p ~/.mozilla/native-messaging-hosts
    cp pagewatch_host.json ~/.mozilla/native-messaging-hosts/
    d. Update paths inside pagewatch_host.py
    1. Change:
    BASE_DIR = r"C:\app\browser\firefox\pagewatch"
    2. To:
    BASE_DIR = "/home/you/browser/firefox/pagewatch"
    e. Make the Python host executable
    1. chmod +x /home/you/browser/firefox/pagewatch/pagewatch_host.py
    f. No changes needed to the extension files
    1. manifest.json
    2. background.js
    3. content.js
    These work the same on Linux.
    g. Optional: Linux can launch GUI applications
    1. If desired, the host can launch gVim or other GUI tools:
    import subprocess
    subprocess.Popen(["gvim", "/tmp/somefile"])
    2. This works on Linux but not on Windows native hosts.
    h. Summary of required changes
    1. Replace the .bat launcher with a .sh script
    2. Update the JSON "path" to point to the .sh script
    3. Install the JSON file into ~/.mozilla/native-messaging-hosts/
    4. Update BASE_DIR in the Python script
    5. Mark both the .py and .sh files executable
    6. Remove the registry installation step

    I did not test the Linux setup so errors may have snuck in.
    --
    I strive to make every post on Usenet add value that wasn't there before.
    Most people invest seconds into their posts, whereas I invest hours.
    --- Synchronet 3.21b-Linux NewsLink 1.2