Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package kl for openSUSE:Factory checked in 
at 2026-03-26 21:08:36
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/kl (Old)
 and      /work/SRC/openSUSE:Factory/.kl.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "kl"

Thu Mar 26 21:08:36 2026 rev:4 rq:1342689 version:0.8.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/kl/kl.changes    2026-03-09 16:23:27.570968204 
+0100
+++ /work/SRC/openSUSE:Factory/.kl.new.8177/kl.changes  2026-03-27 
06:42:25.660442092 +0100
@@ -1,0 +2,15 @@
+Thu Mar 26 05:52:07 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.8.0:
+  * feat: upgrade viewport to v0.11.2
+  * feat: start logs unwrapped, add hint for wrap and prettify
+  * docs: update README dev section
+  * fix: make entity state transitions more resilient to duplicate
+    container deltas
+  * fix: ensure entity tree selection is only maintained after
+    MaintainEntitySelectionAfterFirstContainer
+  * feat: vertically center focused matches in unwrapped logs
+  * chore: update demo
+  * ci: fix and pin linting
+
+-------------------------------------------------------------------

Old:
----
  kl-0.7.0.obscpio

New:
----
  kl-0.8.0.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ kl.spec ++++++
--- /var/tmp/diff_new_pack.miXF56/_old  2026-03-27 06:42:26.792488865 +0100
+++ /var/tmp/diff_new_pack.miXF56/_new  2026-03-27 06:42:26.792488865 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           kl
-Version:        0.7.0
+Version:        0.8.0
 Release:        0
 Summary:        An interactive Kubernetes log viewer for your terminal
 License:        MIT

++++++ _service ++++++
--- /var/tmp/diff_new_pack.miXF56/_old  2026-03-27 06:42:26.848491178 +0100
+++ /var/tmp/diff_new_pack.miXF56/_new  2026-03-27 06:42:26.852491344 +0100
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/robinovitch61/kl.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">refs/tags/v0.7.0</param>
+    <param name="revision">refs/tags/v0.8.0</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.miXF56/_old  2026-03-27 06:42:26.892492996 +0100
+++ /var/tmp/diff_new_pack.miXF56/_new  2026-03-27 06:42:26.900493326 +0100
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/robinovitch61/kl.git</param>
-              <param 
name="changesrevision">0738d45b0fbe2c5bead17d41b55b9eaaa60f7b73</param></service></servicedata>
+              <param 
name="changesrevision">c4be260829fc37183a9963f4ab2194cd26438c6f</param></service></servicedata>
 (No newline at EOF)
 

++++++ kl-0.7.0.obscpio -> kl-0.8.0.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/.golangci.yml new/kl-0.8.0/.golangci.yml
--- old/kl-0.7.0/.golangci.yml  2026-03-06 18:33:21.000000000 +0100
+++ new/kl-0.8.0/.golangci.yml  2026-03-25 04:09:13.000000000 +0100
@@ -20,6 +20,7 @@
         - G108 # pprof endpoint
         - G114 # http serve without timeouts (debug only)
         - G115 # integer overflow conversion (linebuffer math)
+        - G118 # cancel func stored in model, called on shutdown
         - G301 # directory permissions
         - G304 # file path variable (CLI tool, not a server)
         - G401 # weak crypto (md5 used for color hashing, not security)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/README.md new/kl-0.8.0/README.md
--- old/kl-0.7.0/README.md      2026-03-06 18:33:21.000000000 +0100
+++ new/kl-0.8.0/README.md      2026-03-25 04:09:13.000000000 +0100
@@ -169,24 +169,27 @@
 Run an example flask + postgres + nginx setup in a local 
[k3d](https://k3d.io/) cluster for testing locally:
 
 ```sh
-k3d cluster create test
-k3d cluster create test2
-kubectl --context k3d-test apply -f ./dev/deploy.yaml
-kubectl --context k3d-test2 create namespace otherns
-kubectl --context k3d-test2 apply -f ./dev/deploy.yaml -n otherns
+k3d cluster create mycluster
+k3d cluster create myothercluster
+kubectl --context k3d-mycluster apply -f ./dev/deploy.yaml
+kubectl --context k3d-myothercluster create namespace otherns
+kubectl --context k3d-myothercluster apply -f ./dev/deploy.yaml -n otherns
 
-# view both clusters and all namespaces in kl
-kl --context k3d-test,k3d-test2 -A
+# view both clusters and both namespaces in kl
+kl --context k3d-mycluster,k3d-myothercluster -n default,otherns
+
+# use -A to include all namespaces (e.g. kube-system) in each cluster
+kl --context k3d-mycluster,k3d-myothercluster -A
 
 # access the application's webpage
-kubectl --context k3d-test2 -n otherns port-forward services/frontend-service 
8080:80
+kubectl --context k3d-myothercluster -n otherns port-forward 
services/frontend-service 8080:80
 open http://localhost:8080
 
 # browser console one-liner to click button every second to generate logs
 setInterval(() => { document.getElementsByTagName("button")[0].click(); }, 
1000);
 
 # or make requests directly to flask from the terminal
-kubectl --context k3d-test2 port-forward services/flask-service 5000:5000
+kubectl --context k3d-myothercluster port-forward services/flask-service 
5000:5000
 curl http://localhost:5000/status
 ```
 
Binary files old/kl-0.7.0/demo/demo.gif and new/kl-0.8.0/demo/demo.gif differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/dev/deploy.yaml new/kl-0.8.0/dev/deploy.yaml
--- old/kl-0.7.0/dev/deploy.yaml        2026-03-06 18:33:21.000000000 +0100
+++ new/kl-0.8.0/dev/deploy.yaml        2026-03-25 04:09:13.000000000 +0100
@@ -103,8 +103,6 @@
               value: "30"
             - name: PERIODIC_LOGGING
               value: "true"
-            - name: PERIODIC_LOGGING_BYTES_PER_LOG
-              value: "10000"
             - name: PERIODIC_LOGGING_LOGS_PER_SECOND
               value: "5"
             - name: PERIODIC_BIG_LOGGING
@@ -140,8 +138,6 @@
               value: "30"
             - name: PERIODIC_LOGGING
               value: "true"
-            - name: PERIODIC_LOGGING_BYTES_PER_LOG
-              value: "10000"
             - name: PERIODIC_LOGGING_LOGS_PER_SECOND
               value: "5"
             - name: PERIODIC_BIG_LOGGING
@@ -177,8 +173,6 @@
               value: "30"
             - name: PERIODIC_LOGGING
               value: "true"
-            - name: PERIODIC_LOGGING_BYTES_PER_LOG
-              value: "10000"
             - name: PERIODIC_LOGGING_LOGS_PER_SECOND
               value: "5"
             - name: PERIODIC_BIG_LOGGING
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/dev/flask/app.py 
new/kl-0.8.0/dev/flask/app.py
--- old/kl-0.7.0/dev/flask/app.py       2026-03-06 18:33:21.000000000 +0100
+++ new/kl-0.8.0/dev/flask/app.py       2026-03-25 04:09:13.000000000 +0100
@@ -12,25 +12,22 @@
 from psycopg2 import pool
 
 
-class JSONFormatter(logging.Formatter):
+class MixedFormatter(logging.Formatter):
+    _plain = logging.Formatter("%(asctime)s %(levelname)-8s %(message)s", 
datefmt="%Y-%m-%dT%H:%M:%S")
+
     def format(self, record):
-        log_record = {
-            "timestamp": datetime.utcnow().isoformat(),
-            "level": record.levelname,
-            "message": record.getMessage(),
-            "module": record.module,
-            "function": record.funcName,
-            "line": record.lineno
-        }
-        if record.exc_info:
-            log_record["exception"] = self.formatException(record.exc_info)
-        return json.dumps(log_record)
+        msg = record.getMessage()
+        try:
+            json.loads(msg)
+            return msg
+        except (ValueError, TypeError):
+            return self._plain.format(record)
 
-logger = logging.getLogger()
-logger.setLevel(logging.DEBUG)
 handler = logging.StreamHandler()
-handler.setFormatter(JSONFormatter())
-logger.addHandler(handler)
+handler.setFormatter(MixedFormatter())
+logging.root.setLevel(logging.DEBUG)
+logging.root.addHandler(handler)
+logger = logging.getLogger(__name__)
 
 app = Flask(__name__)
 CORS(app)
@@ -90,35 +87,83 @@
     exit_thread = threading.Thread(target=exit_after_delay, daemon=True)
     exit_thread.start()
 
-def generate_random_text(num_bytes, prefix=""):
-    words = ['hello', 'world', 'test', 'random', 'text', 'words', 'generator',
-             'python', 'code', 'sample']
-    # choose words randomly, returning exactly num_bytes of text
-    res = prefix
-    while len(res) < num_bytes:
-        res += random.choice(words) + " "
-    return res[:num_bytes]
+_PLAIN_ENTRIES = [
+    (logging.DEBUG,   "cache lookup: key=user:1042 hit=True ttl=298s"),
+    (logging.DEBUG,   "db query took 3ms: SELECT * FROM users WHERE id=$1"),
+    (logging.DEBUG,   "db query took 18ms: SELECT count(*) FROM orders WHERE 
status='pending'"),
+    (logging.DEBUG,   "cache miss: key=session:abc123, fetching from db"),
+    (logging.DEBUG,   "worker thread pool: 4/16 threads active"),
+    (logging.DEBUG,   "config loaded: POSTGRES_HOST=postgres-service 
PORT=5000"),
+    (logging.INFO,    "GET /api/v1/users 200 14ms"),
+    (logging.INFO,    "POST /api/v1/orders 201 22ms"),
+    (logging.INFO,    "GET /api/v1/products?category=electronics 200 31ms"),
+    (logging.INFO,    "DELETE /api/v1/sessions/xyz 204 5ms"),
+    (logging.INFO,    "user 1042 logged in from 203.0.113.47"),
+    (logging.INFO,    "order #8821 created for user 1042, total=$142.50"),
+    (logging.INFO,    "email queued: [email protected] -> user:5510"),
+    (logging.INFO,    "scheduled job 'cleanup_expired_sessions' completed, 
removed 14 rows"),
+    (logging.INFO,    "db pool: 3/20 connections in use"),
+    (logging.WARNING, "slow query (1.2s): SELECT orders JOIN users ON 
users.id=orders.user_id"),
+    (logging.WARNING, "rate limit approaching for ip 198.51.100.22: 87/100 
req/min"),
+    (logging.WARNING, "jwt token expiring soon for user 2201, issuing 
refresh"),
+    (logging.WARNING, "disk usage at 81% on /var/data, threshold is 85%"),
+    (logging.WARNING, "retrying db connection (attempt 2/3): connection 
refused"),
+    (logging.ERROR,   "failed to send email to user:3309: SMTP timeout after 
10s"),
+    (logging.ERROR,   "unhandled exception in worker: ZeroDivisionError: 
division by zero"),
+    (logging.ERROR,   "payment gateway returned 502, order #9001 not 
processed"),
+    (logging.ERROR,   "db write failed: deadlock detected, rolling back 
transaction"),
+]
+
+_JSON_ENTRIES = [
+    (logging.DEBUG,  {"event": "cache_get",  "key": "user:2048",     "hit": 
True,  "ttl": 120}),
+    (logging.DEBUG,  {"event": "query",      "sql": "SELECT * FROM sessions 
WHERE token=$1", "ms": 2}),
+    (logging.INFO,   {"event": "request",    "method": "GET",  "path": 
"/health",          "status": 200, "ms": 4}),
+    (logging.INFO,   {"event": "request",    "method": "POST", "path": 
"/api/v1/login",    "status": 200, "ms": 37, "user_id": 1042}),
+    (logging.INFO,   {"event": "request",    "method": "GET",  "path": 
"/api/v1/orders",   "status": 200, "ms": 55, "count": 23}),
+    (logging.INFO,   {"event": "job_done",   "job": "send_digest_emails", 
"sent": 312, "failed": 1}),
+    (logging.INFO,   {"event": "db_pool",    "active": 5, "idle": 15, "max": 
20}),
+    (logging.WARNING,{"event": "slow_query", "ms": 980, "sql": "UPDATE orders 
SET status=$1 WHERE user_id=$2"}),
+    (logging.WARNING,{"event": "rate_limit", "ip": "203.0.113.99", "requests": 
95, "limit": 100}),
+    (logging.ERROR,  {"event": "exception",  "type": "ConnectionError", "msg": 
"db connection lost", "attempt": 1}),
+    (logging.ERROR,  {"event": "request",    "method": "POST", "path": 
"/api/v1/checkout",  "status": 500, "ms": 102, "error": "payment timeout"}),
+]
+
+def _pick_log_entry():
+    if random.random() < 0.35:
+        level, data = random.choice(_JSON_ENTRIES)
+        return level, json.dumps(data)
+    else:
+        return random.choice(_PLAIN_ENTRIES)
 
 
 def periodic_logger():
-    num_bytes = int(os.getenv("PERIODIC_LOGGING_BYTES_PER_LOG", "1000"))
     logs_per_second = int(os.getenv("PERIODIC_LOGGING_LOGS_PER_SECOND", "1"))
-    delay = 1/logs_per_second
+    delay = 1 / logs_per_second
     while True:
-        logger.info(generate_random_text(num_bytes))
+        level, msg = _pick_log_entry()
+        logger.log(level, msg)
         time.sleep(delay)
 
-# Periodically log random text
+# Periodically emit realistic log entries
 if os.getenv("PERIODIC_LOGGING"):
     logging_thread = threading.Thread(target=periodic_logger, daemon=True)
     logging_thread.start()
 
+def _make_long_text(num_bytes):
+    words = ["processing", "request", "user", "order", "session", "cache",
+             "database", "index", "query", "worker", "event", "payload",
+             "stream", "record", "batch", "token", "config", "metric"]
+    res = "long "
+    while len(res) < num_bytes:
+        res += random.choice(words) + " "
+    return res[:num_bytes]
+
 def periodic_big_logger():
     num_bytes = int(os.getenv("PERIODIC_BIG_LOGGING_BYTES_PER_LOG", "500_000"))
     logs_per_second = int(os.getenv("PERIODIC_BIG_LOGGING_LOGS_PER_SECOND", 
"0.05"))
-    delay = 1/logs_per_second
+    delay = 1 / logs_per_second
     while True:
-        logger.info(generate_random_text(num_bytes, prefix="long "))
+        logger.info(_make_long_text(num_bytes))
         time.sleep(delay)
 
 # Periodically log really long random text
@@ -128,10 +173,8 @@
 
 @app.route("/health")
 def health():
-    logger.info("Health check endpoint called")
-    logger.info("FIRST Lorem ipsum \n\tdolor sit amet, consectetur adipiscing 
elit. \n\tSed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut 
enim \n\tad minim veniam, quis nostrud exercitation ullamco laboris nisi ut 
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in 
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 
occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim 
id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem 
accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo 
inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo 
enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia 
consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.")
-    logger.info("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed 
do eiusmod \n\ttempor incididunt ut labore et dolore magna aliqua. \n\tUt enim 
ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex 
ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit 
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat 
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium 
doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore 
veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim 
ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia 
consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.")
-    logger.info("LAST Lorem ipsum \n\tdolor sit amet, consectetur adipiscing 
elit. \n\tSed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut 
enim ad \n\tminim veniam, quis nostrud exercitation ullamco laboris nisi ut 
aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in 
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint 
occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim 
id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem 
accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo 
inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo 
enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia 
consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.")
+    logger.debug("health check: db pool active=%d idle=%d", 3, 17)
+    logger.info("GET /health 200 2ms")
     return jsonify({"status": "healthy"}), 200
 
 @app.route("/status", methods=["GET"])
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/go.mod new/kl-0.8.0/go.mod
--- old/kl-0.7.0/go.mod 2026-03-06 18:33:21.000000000 +0100
+++ new/kl-0.8.0/go.mod 2026-03-25 04:09:13.000000000 +0100
@@ -17,7 +17,7 @@
        github.com/mattn/go-runewidth v0.0.20
        github.com/muesli/reflow v0.3.0
        github.com/rivo/uniseg v0.4.7
-       github.com/robinovitch61/viewport v0.11.0
+       github.com/robinovitch61/viewport v0.11.2
        github.com/spf13/cobra v1.8.0
        github.com/spf13/pflag v1.0.5
        github.com/spf13/viper v1.18.2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/go.sum new/kl-0.8.0/go.sum
--- old/kl-0.7.0/go.sum 2026-03-06 18:33:21.000000000 +0100
+++ new/kl-0.8.0/go.sum 2026-03-25 04:09:13.000000000 +0100
@@ -123,8 +123,8 @@
 github.com/rivo/uniseg v0.2.0/go.mod 
h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod 
h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-github.com/robinovitch61/viewport v0.11.0 
h1:SHyFpbVt1j2GpbstdYNAE44tAWSOE3dCYFM2CcR19/M=
-github.com/robinovitch61/viewport v0.11.0/go.mod 
h1:1Pvc8fjY7bgra0mGEsDD1kK6ECqBxT04DuEsOCzE5OY=
+github.com/robinovitch61/viewport v0.11.2 
h1:PuVv+MLJp1LXanYX/fS40hztW2OzlXQAxvOD+hBow38=
+github.com/robinovitch61/viewport v0.11.2/go.mod 
h1:1Pvc8fjY7bgra0mGEsDD1kK6ECqBxT04DuEsOCzE5OY=
 github.com/rogpeppe/go-internal v1.10.0 
h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/rogpeppe/go-internal v1.10.0/go.mod 
h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod 
h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/internal/app.go new/kl-0.8.0/internal/app.go
--- old/kl-0.7.0/internal/app.go        2026-03-06 18:33:21.000000000 +0100
+++ new/kl-0.8.0/internal/app.go        2026-03-25 04:09:13.000000000 +0100
@@ -819,19 +819,14 @@
 
        if len(existingContainerEntities) == 0 && !m.state.seenFirstContainer {
                m.pages[m.state.focusedPageType] = 
m.pages[m.state.focusedPageType].WithFocus()
-               cmds = append(cmds, 
tea.Tick(constants.AttemptMaintainEntitySelectionAfterFirstContainer, func(t 
time.Time) tea.Msg { return message.StartMaintainEntitySelectionMsg{} }))
+               cmds = append(cmds, 
tea.Tick(constants.MaintainEntitySelectionAfterFirstContainer, func(t 
time.Time) tea.Msg { return message.StartMaintainEntitySelectionMsg{} }))
                m.state.seenFirstContainer = true
        }
 
        for _, delta := range msg.DeltaSet.OrderedDeltas() {
-               // get the existing entity for the container, if it exists
-               var existingContainerEntity *entity.Entity
-               for _, containerEntity := range existingContainerEntities {
-                       if containerEntity.Container.Equals(delta.Container) {
-                               existingContainerEntity = &containerEntity
-                               break
-                       }
-               }
+               // get the existing entity for the container from the current 
tree, not a stale snapshot,
+               // as prior iterations may have changed the entity's state
+               existingContainerEntity := 
m.entityTree.GetEntity(delta.Container)
 
                if delta.ToDelete {
                        if existingContainerEntity != nil {
@@ -866,14 +861,7 @@
 func (m Model) handleStartedLogScannerMsg(msg command.StartedLogScannerMsg) 
(Model, tea.Cmd) {
        var cmd tea.Cmd
        var cmds []tea.Cmd
-       existingContainerEntities := m.entityTree.GetContainerEntities()
-       var startedContainerEntity *entity.Entity
-       for _, containerEntity := range existingContainerEntities {
-               if msg.LogScanner.Container.Equals(containerEntity.Container) {
-                       startedContainerEntity = &containerEntity
-                       break
-               }
-       }
+       startedContainerEntity := 
m.entityTree.GetEntity(msg.LogScanner.Container)
        if startedContainerEntity == nil {
                msg.LogScanner.Cancel()
                return m, nil
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/internal/app_test.go 
new/kl-0.8.0/internal/app_test.go
--- old/kl-0.7.0/internal/app_test.go   2026-03-06 18:33:21.000000000 +0100
+++ new/kl-0.8.0/internal/app_test.go   2026-03-25 04:09:13.000000000 +0100
@@ -226,3 +226,71 @@
                t.Errorf("expected view to contain cluster name 'test-cluster', 
got:\n%s", view)
        }
 }
+
+func TestDuplicateDeltas_NoDoubleStartScanner(t *testing.T) {
+       m := newTestModel()
+
+       // create a container in WantScanning state (waiting + activated)
+       ct := newAppTestContainer()
+       ct.Status.State = container.ContainerWaiting
+       var createSet container.ContainerDeltaSet
+       createSet.Add(newAppTestDelta(ct, true))
+       m = updateModel(t, m, command.GetContainerDeltasMsg{DeltaSet: 
createSet})
+
+       ent := m.entityTree.GetEntity(ct)
+       if ent == nil {
+               t.Fatal("expected entity to exist")
+       }
+       if ent.State != entity.WantScanning {
+               t.Fatalf("expected WantScanning, got %v", ent.State)
+       }
+
+       // send two update deltas for the same container transitioning to 
Running in the same batch
+       runningCt := ct
+       runningCt.Status.State = container.ContainerRunning
+       var updateSet container.ContainerDeltaSet
+       updateSet.Add(container.ContainerDelta{
+               Time:      time.Now(),
+               Container: runningCt,
+       })
+       updateSet.Add(container.ContainerDelta{
+               Time:      time.Now().Add(time.Millisecond),
+               Container: runningCt,
+       })
+       m = updateModel(t, m, command.GetContainerDeltasMsg{DeltaSet: 
updateSet})
+
+       ent = m.entityTree.GetEntity(runningCt)
+       if ent == nil {
+               t.Fatal("expected entity to exist")
+       }
+       if ent.State != entity.ScannerStarting {
+               t.Fatalf("expected ScannerStarting, got %v", ent.State)
+       }
+
+       // simulate first scanner starting successfully
+       _, cancel1 := context.WithCancel(context.Background())
+       scanner1 := k8s_log.NewLogScanner(runningCt, nil, cancel1, nil)
+       m = updateModel(t, m, command.StartedLogScannerMsg{LogScanner: 
scanner1})
+
+       ent = m.entityTree.GetEntity(runningCt)
+       if ent == nil {
+               t.Fatal("expected entity to exist")
+       }
+       if ent.State != entity.Scanning {
+               t.Fatalf("expected Scanning, got %v", ent.State)
+       }
+
+       // if a duplicate StartScanner was dispatched, a second ScannerStarted 
would arrive
+       // for an entity already in Scanning state, which must not panic
+       _, cancel2 := context.WithCancel(context.Background())
+       scanner2 := k8s_log.NewLogScanner(runningCt, nil, cancel2, nil)
+       m = updateModel(t, m, command.StartedLogScannerMsg{LogScanner: 
scanner2})
+
+       ent = m.entityTree.GetEntity(runningCt)
+       if ent == nil {
+               t.Fatal("expected entity to exist")
+       }
+       if ent.State != entity.Scanning {
+               t.Fatalf("expected Scanning, got %v", ent.State)
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/internal/constants/constants.go 
new/kl-0.8.0/internal/constants/constants.go
--- old/kl-0.7.0/internal/constants/constants.go        2026-03-06 
18:33:21.000000000 +0100
+++ new/kl-0.8.0/internal/constants/constants.go        2026-03-25 
04:09:13.000000000 +0100
@@ -53,9 +53,8 @@
 // InitialLookbackMins controls the initial number of minutes to look back in 
logs
 var InitialLookbackMins = 1
 
-// AttemptMaintainEntitySelectionAfterFirstContainer controls how long to 
delay after the first container before
-// attempting to maintain the currently selected Entity in the tree. The 
thinking goes that the tree may not be fully
-// populated yet and the user won't even have time to orient themselves and 
then the selection is somewhere in the
-// middle of the tree. But after a short amount of time, they will have 
actively selected something and we can try to
-// maintain that selection
-var AttemptMaintainEntitySelectionAfterFirstContainer = 1 * time.Second
+// MaintainEntitySelectionAfterFirstContainer controls how long to delay after 
the first container before maintaining
+// the currently selected Entity in the tree. The thinking goes that the tree 
may not be fully populated yet and the
+// user won't even have time to orient themselves and then the selection is 
somewhere in the middle of the tree. But
+// after a short amount of time, they will have actively selected something 
and we can try to maintain that selection.
+var MaintainEntitySelectionAfterFirstContainer = 1 * time.Second
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/internal/k8s/entity/entity.go 
new/kl-0.8.0/internal/k8s/entity/entity.go
--- old/kl-0.7.0/internal/k8s/entity/entity.go  2026-03-06 18:33:21.000000000 
+0100
+++ new/kl-0.8.0/internal/k8s/entity/entity.go  2026-03-25 04:09:13.000000000 
+0100
@@ -298,6 +298,10 @@
 
                tree.AddOrReplace(e)
                return e, tree, []EntityAction{}
+       case Scanning:
+               // can happen when duplicate StartScanner actions are 
dispatched from a single delta batch
+               scanner.Cancel()
+               return e, tree, []EntityAction{}
        case Deleted:
                // can happen when a same-batch 
Update(WantScanning→ScannerStarting) dispatches StartScanner,
                // then a Delete(WantScanning→Deleted) overwrites the state 
before the scanner starts
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/internal/k8s/entity/entity_test.go 
new/kl-0.8.0/internal/k8s/entity/entity_test.go
--- old/kl-0.7.0/internal/k8s/entity/entity_test.go     2026-03-06 
18:33:21.000000000 +0100
+++ new/kl-0.8.0/internal/k8s/entity/entity_test.go     2026-03-25 
04:09:13.000000000 +0100
@@ -520,8 +520,21 @@
        assertActions(t, actions, []entity.EntityAction{})
 }
 
+func TestScannerStarted_AlreadyScanning(t *testing.T) {
+       tree := newTestTree()
+       ent := newTestEntity(entity.Scanning, container.ContainerRunning)
+       tree.AddOrReplace(ent)
+
+       scanner := newTestScanner()
+       result, _, actions := ent.ScannerStarted(tree, nil, scanner)
+
+       // should stay Scanning and cancel the duplicate scanner
+       assertState(t, result, entity.Scanning)
+       assertActions(t, actions, []entity.EntityAction{})
+}
+
 func TestScannerStarted_FromInvalidState_Panics(t *testing.T) {
-       for _, state := range []entity.EntityState{entity.Inactive, 
entity.WantScanning, entity.Scanning, entity.ScannerStopping} {
+       for _, state := range []entity.EntityState{entity.Inactive, 
entity.WantScanning, entity.ScannerStopping} {
                t.Run(state.String(), func(t *testing.T) {
                        tree := newTestTree()
                        ent := newTestEntity(state, container.ContainerRunning)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/internal/page/entities.go 
new/kl-0.8.0/internal/page/entities.go
--- old/kl-0.7.0/internal/page/entities.go      2026-03-06 18:33:21.000000000 
+0100
+++ new/kl-0.8.0/internal/page/entities.go      2026-03-25 04:09:13.000000000 
+0100
@@ -60,9 +60,9 @@
                        Top:          keyMap.Top,
                        Bottom:       keyMap.Bottom,
                }),
+               viewport.WithSelectionEnabled[entity.Entity](true),
+               viewport.WithWrapText[entity.Entity](false),
        )
-       vp.SetSelectionEnabled(true)
-       vp.SetWrapText(false)
 
        fvp := filterableviewport.New(vp,
                
filterableviewport.WithKeyMap[entity.Entity](filterableviewport.KeyMap{
@@ -200,9 +200,6 @@
        f := p.getCurrentFilter()
        p.entityTree.UpdatePrettyPrintPrefixes(f)
        p.filterableViewport.SetObjects(p.entityTree.GetVisibleEntities(f))
-       p.filterableViewport.SetSelectionComparator(func(a, b entity.Entity) 
bool {
-               return a.EqualTo(b)
-       })
        p.filterableViewport.SetAdjustObjectsForFilter(func(filterText string, 
isRegex bool) []entity.Entity {
                f := makeFilterFromText(filterText, isRegex, p.keyMap)
                entityTree.UpdatePrettyPrintPrefixes(f)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/internal/page/log.go 
new/kl-0.8.0/internal/page/log.go
--- old/kl-0.7.0/internal/page/log.go   2026-03-06 18:33:21.000000000 +0100
+++ new/kl-0.8.0/internal/page/log.go   2026-03-25 04:09:13.000000000 +0100
@@ -74,9 +74,9 @@
                        Bottom:       keyMap.Bottom,
                }),
                
viewport.WithSelectionStyleOverridesItemStyle[SingleLogLine](false),
+               viewport.WithSelectionEnabled[SingleLogLine](false),
+               viewport.WithWrapText[SingleLogLine](true),
        )
-       vp.SetSelectionEnabled(false)
-       vp.SetWrapText(true)
 
        fvp := filterableviewport.New(vp,
                
filterableviewport.WithKeyMap[SingleLogLine](filterableviewport.KeyMap{
@@ -96,6 +96,7 @@
                filterableviewport.WithEmptyText[SingleLogLine]("'/', 'r', or 
'i' to filter"),
                
filterableviewport.WithFilterLinePosition[SingleLogLine](filterableviewport.FilterLineTop),
                filterableviewport.WithFilterLinePrefix[SingleLogLine]("Single 
Log"),
+               filterableviewport.WithHorizontalPad[SingleLogLine](100000), // 
effectively center matches when text is unwrapped
                
filterableviewport.WithStyles[SingleLogLine](filterableviewport.Styles{
                        Match: filterableviewport.MatchStyles{
                                Focused:           theme.MatchFocused,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/internal/page/logs.go 
new/kl-0.8.0/internal/page/logs.go
--- old/kl-0.7.0/internal/page/logs.go  2026-03-06 18:33:21.000000000 +0100
+++ new/kl-0.8.0/internal/page/logs.go  2026-03-25 04:09:13.000000000 +0100
@@ -59,9 +59,9 @@
                        Bottom:       keyMap.Bottom,
                }),
                
viewport.WithSelectionStyleOverridesItemStyle[model.PageLog](false),
+               viewport.WithSelectionEnabled[model.PageLog](true),
+               viewport.WithWrapText[model.PageLog](false),
        )
-       vp.SetSelectionEnabled(true)
-       vp.SetWrapText(true)
 
        fvp := filterableviewport.New(vp,
                
filterableviewport.WithKeyMap[model.PageLog](filterableviewport.KeyMap{
@@ -81,6 +81,7 @@
                filterableviewport.WithEmptyText[model.PageLog]("'/', 'r', or 
'i' to filter"),
                
filterableviewport.WithFilterLinePosition[model.PageLog](filterableviewport.FilterLineTop),
                
filterableviewport.WithFilterLinePrefix[model.PageLog](fmt.Sprintf("(L)ogs, 
%s", getOrder(!descending))),
+               filterableviewport.WithHorizontalPad[model.PageLog](100000), // 
effectively center matches when text is unwrapped
                
filterableviewport.WithStyles[model.PageLog](filterableviewport.Styles{
                        Match: filterableviewport.MatchStyles{
                                Focused:           theme.MatchFocused,
@@ -390,6 +391,7 @@
 func (p *LogsPage) updateFilterLabel() {
        prefix := fmt.Sprintf("(L)ogs, %s", 
getOrder(p.logContainer.Ascending()))
        if p.focused {
+               prefix += " [(w)rap, (p)rettify]"
                prefix = p.theme.FilterPrefixFocused.Render(prefix)
        }
        p.filterableViewport.SetFilterLinePrefix(prefix)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kl-0.7.0/internal/textinput/README.md 
new/kl-0.8.0/internal/textinput/README.md
--- old/kl-0.7.0/internal/textinput/README.md   2026-03-06 18:33:21.000000000 
+0100
+++ new/kl-0.8.0/internal/textinput/README.md   2026-03-25 04:09:13.000000000 
+0100
@@ -1,2 +1,2 @@
-This textinput is forked from https://github.com/charmbracelet/bubbles in 
order to add some utility functions, e.g.
-`IsEmpty`
\ No newline at end of file
+This textinput is forked from https://github.com/charmbracelet/bubbles in 
order to add some utility functions for
+performance, e.g. `IsEmpty`
\ No newline at end of file

++++++ kl.obsinfo ++++++
--- /var/tmp/diff_new_pack.miXF56/_old  2026-03-27 06:42:27.352512003 +0100
+++ /var/tmp/diff_new_pack.miXF56/_new  2026-03-27 06:42:27.356512167 +0100
@@ -1,5 +1,5 @@
 name: kl
-version: 0.7.0
-mtime: 1772818401
-commit: 0738d45b0fbe2c5bead17d41b55b9eaaa60f7b73
+version: 0.8.0
+mtime: 1774408153
+commit: c4be260829fc37183a9963f4ab2194cd26438c6f
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/kl/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.kl.new.8177/vendor.tar.gz differ: char 19, line 1

Reply via email to