portal

practicalli-johnny 2023-04-21T14:27:47.681949Z

Is there a way to detect if Portal is already running and only open a new portal window if one is not already running? In my workflow, I use a dev/user.clj file to automatically start portal when developing a Clojure project.

;; Open Portal window in browser with dark theme
(inspect/open {:portal.colors/theme :portal.colors/gruvbox})
;; Add portal as a tap> target and listen to all evaluation results (via nREPL)
(add-tap #'portal.api/submit) 
I use reload / restart functions (clojure.tools.namespace or IntegrantREPL) when there are significant changes and this is causing my dev/user.clj to be reloaded and a new Portal is opened If there isnt a way to detect if portal is already connected, then I could • exclude dev from the reload via clojure.tools.namespace.repl/set-refresh-dirs - (set-refresh-dirs "src" "resources") although sometime its handy to include dev code, but that can be re-evaluated easily enough. • manually close portal (I'll probably forget 😿 ) • add code to the restart to close portal (possibly leading to a bit of coupling I'd rather not have) Any other options you can think of? Thanks.

djblue 2023-04-21T15:14:47.225399Z

I think you can pass the return value of open back into open to avoid opening multiple windows.

practicalli-johnny 2023-04-21T15:46:19.338009Z

I am afraid I dont seem to know how to do what was suggested, as the following opens two portal windows and when I refresh I get two more laughcry

(def portal-instance (inspect/open {:portal.colors/theme :portal.colors/gruvbox}))
(inspect/open portal-instance)

djblue 2023-04-21T15:48:10.625289Z

Ohh, just remembered refresh will remove all the vars in the ns. You can call portal.api/sessions to get all currently connected sessions.

👀 1
djblue 2023-04-21T17:06:49.975899Z

(def portal-instance
  (or (first (inspect/sessions))
      (inspect/open {:portal.colors/theme :portal.colors/gruvbox})))
Should be enough I think 🤔

practicalli-johnny 2023-04-21T17:20:50.691019Z

Pretty close, although now it taps everything twice to portal 😆 so if I bring the add tap into the or form then it should work okay I believe..

practicalli-johnny 2023-04-21T17:25:51.062089Z

Hmm, I tried this but its still showing duplicate results in portal, each time refresh is used then another duplicate tap is added... curious..

(def portal-instance
  (or (first (inspect/sessions))
      (do 
        (inspect/open {:portal.colors/theme :portal.colors/gruvbox})
        (add-tap #'portal.api/submit))))

practicalli-johnny 2023-04-21T17:52:51.017349Z

Curious. If I (remove-tap #'portal.api/submit) then portal no longer received the data (expected) Although a new (add-tap #'portal.api/submit) will send duplicates to portal (based on how many times restart was used) So for now I could wrap portal in a function in the dev/user.clj and it can be called by the developer. This should avoid reloading effecting portal. Or using (set-refresh-dirs "src" "resources") also worked well and is very simple, so I could use that approach instead. I'll think about the need to reload dev code (I dont think its that important) and I can always update with set-refresh-dirs before doing a refresh. Thanks for the advice.

👍 1
practicalli-johnny 2023-04-21T21:58:44.584589Z

I can ensure that there is only one instance of Portal window

(def portal-instance
  (or (seq (inspect/sessions))
      (inspect/open {:portal.colors/theme :portal.colors/gruvbox})))
But is there something I can check to see if Portal has already been added as a tap source? If so, then that would allow automatic loading in the dev/user.clj file without duplication. If that would be interesting, I can raise an issue. Otherwise I will stick with the set-refresh-dirs Thanks.

djblue 2023-04-21T22:54:20.638659Z

The tap source info is only known by clojure.core 🤔

practicalli-johnny 2023-04-21T23:37:58.151109Z

After some more testing, I think its the mulog tap publisher and/or mulog trace middleware I'm using that may be causing the duplication, rather than add-tap over nrepl. The duplications are when I am testing an API via the swagger interface and I assume the mulog-middleware Without taping all the mulog logs, I dont seem to see any duplications... its been a useful exercise to learn more about tap and porta.

practicalli-johnny 2023-04-21T23:39:31.323849Z

I can access the value in the private var clojure.core/tapset which is an atom that contains a set. Although setting a condidtion does not make a difference, which further suggests its the mulog publisher / middleware

(when (empty? (deref (deref #'clojure.core/tapset)))
  (add-tap #'portal.api/submit))

practicalli-johnny 2023-04-21T23:43:30.212519Z

I remember having duplication with mulog previously, when I forgot to close the publisher, so it seems I need to shut down the publisher before a restart (or ignore the dev directory)

practicalli-johnny 2023-04-22T01:11:21.485049Z

There is a journal of my experiments in the following issue https://github.com/practicalli/project-templates/issues/7 In summary, I'll exclude the code in dev , although I have code that does allow for automatic startup and reloading together now Thanks again for the help and advice

👍 1
p-himik 2023-04-21T18:51:02.822439Z

Is there a way to filter the tap list in the UI? I just tapped a whole bunch of values with the same exact shape and would like to narrow down the scope of what I'm looking at by e.g. (filter #(= (:kind %) "x") the-tap-list). With a possibility to further narrow it done, as if I was chaining multiple filters.

djblue 2023-04-21T18:53:59.232839Z

Currently, you can select any value and filter based on string matching. https://github.com/djblue/portal/blob/master/src/portal/ui/filter.cljc#L35

djblue 2023-04-21T18:55:29.903599Z

You might have more success pulling the value back into your repl by derefing the portal atom returned by portal.api/open or getting your portal atom from portal.api/sessions

djblue 2023-04-21T18:56:59.623349Z

I think things could be improved, just haven't found something I liked yet 🤔

p-himik 2023-04-21T18:58:11.771539Z

Ah, this might work. I have trouble being effective with the current UI. Apart from not being able to use plain Clojure code for filtering or maybe even transforming values, it keeps losing selection and current scrolling position when new values arrive. But I just got started with using it for something more or less serious so maybe I haven't figured out how to set it up correctly yet.

djblue 2023-04-21T18:59:29.360249Z

I have been considering adding the ability to pause updates which might help with that 🤔

p-himik 2023-04-21T19:01:56.665039Z

> haven't found something I liked yet How about plain code? :) It doesn't have to look pretty or have all the affordances of a fully fledged editor. But having it there would be quite useful. And if you want a neat list, it can be represented as items in the xform list of into. Just like you can have

(into (empty tap-list)
      (comp
        (filter #(= (:kind %) :item))
        (keep #(when (even? (:value %)) (update % :position inc)))
        (remove #(odd? (:position %))))
      tap-list)
the list in the UI could only have three items:
(filter #(= (:kind %) :item))
(keep #(when (even? (:value %)) (update % :position inc)))
(remove #(odd? (:position %))))

djblue 2023-04-21T19:03:51.157419Z

My main issue with code is I would rather users stay in their editor when writing it

seancorfield 2023-04-21T19:04:16.803089Z

@p-himik I have ctrl+alt+space q bound in VS Code to a fragment that lets me input Clojure code and evaluate it -- and *v is shorthand for "the most recent tap>'d value"

p-himik 2023-04-21T19:04:30.280949Z

Another thing that I would personally find immensely useful - marking values and having a separate panel where I can view just those items and nothing else.

seancorfield 2023-04-21T19:04:45.949809Z

That pops open a Calva REPL prompt where I can enter an expression 🙂

💯 1
djblue 2023-04-21T19:04:59.746389Z

I would try portal.api/inspect to open a new portal with a selected value

djblue 2023-04-21T19:05:30.009839Z

It should already be registered as a command in the ui, it will even keep the options used for the current session

djblue 2023-04-21T19:09:21.790829Z

I think my other issue with code is what runtime would you expect it to be running in? the browser or your app runtime?

seancorfield 2023-04-21T19:12:57.982389Z

FWIW, I try to do everything via the app runtime, including driving the Portal UI where possible.

➕ 1
p-himik 2023-04-21T19:15:33.820829Z

@seancorfield That would work, although in my case it's "all tapped values", and if I understand correctly that is also available, so I'll try it. @djblue Regarding inspect - that's not quite it. It does open a new window, every single time. And you can't easily distinguish those windows. Of course, I could select a value in Portal, get it back in my REPL, store it somewhere, assign some meaningful label to it, repeat 10 more times, constantly switching back and forth between Portal and REPL, and finally tap the resulting map or whatever I'll end up with. Quite tedious. Compare it to how you'd try to make sense of e.g. a printed out scientific paper on 15 pages. Some pages are completely irrelevant to your inquiry - you put them aside with an always present option to get a second look at them. Some pages have clearly interesting parts - you circle and highlight them. Some disparate pages have parts that belong together - you put those papers close to each other and label the circled/highlighted items in the same way or use the same highlight color. And so on an so forth, all while not leaving the paper medium. I might be "Bret Victor"ing too much right now, but something like that would be perfect. :)

seancorfield 2023-04-21T19:20:44.047239Z

@p-himik https://github.com/seancorfield/vscode-calva-setup/blob/develop/calva/config.edn -- happy to answer any Qs about any of this code (via DM if you want, else here). In particular, the "start Portal" code is in that file too, and uses two Portal windows...

seancorfield 2023-04-21T19:21:57.032689Z

... I also wire up clojure.tools.logging to Portal and use the nREPL middleware: see https://github.com/seancorfield/dot-clojure/blob/develop/src/org/corfield/dev/repl.clj

djblue 2023-04-21T19:24:45.643289Z

For that use case, I would do something like:

(require '[portal.api :as p])

(def bookmarks
  (atom ^{:portal.viewer/default :portal.viewer/inspector} []))

(defn bookmark-add [v]
  (swap! bookmarks conj v))

(defn bookmark-remove [v]
  (swap! bookmarks #(into [] (remove #{v}) %)))

(defn open-bookmarks []
  (p/open {:window-title "bookmarks" :value bookmarks}))

(p/register! #'bookmark-add)
(p/register! #'bookmark-remove)

djblue 2023-04-21T19:26:19.474749Z

You could any value at the repl or in any portal to your book marks

djblue 2023-04-21T19:27:20.467689Z

You have complete control to do any transformations / annotations that you might find useful as well

djblue 2023-04-21T19:28:00.039419Z

You could even do a little bit of layout via hiccup if you wanted

seancorfield 2023-04-21T19:28:06.561449Z

For me, learning that you can have a "Portal instance" attached to any atom containing values was quite a revelation...

djblue 2023-04-21T19:30:54.142759Z

Yeah, the :value option isn't super obvious how powerful it is

p-himik 2023-04-21T19:35:39.390029Z

@seancorfield Sean, that's 450 lines dense with domain-specific things... And I believe that it's all immensely useful lines! But wouldn't you agree that having a few UI buttons would be a better affordance? :) And it's alright if I end up having to dig through them to make it all work. I probably will, at some point, once I'm more familiar with Portal and perhaps after I will have migrated to Calva ("any minute now"). It's just that I have a somewhat pathologic tendency to think of how other people would do the same thing. We wouldn't get too far if we were still stuck with having to deal with spreadsheets in TUI. @djblue Thanks! That's definitely useful. I still have that itch though, nagging at me that it all should be a part of the default UI - after all, we have bookmarks pretty much everywhere. Even IDEA has "Favorites" with different sections. And it all feels like it's just asking for a deeper editor integration where possible. Would love to do some experiments - shame there's just 24 hours in a day.

seancorfield 2023-04-21T19:40:28.370319Z

I don't like buttons 🙂 I prefer "command-line" and "raw code" approaches. Portal's great for visualizing stuff but I pretty much never interact with the UI itself -- and even when I do, it's all via the keyboard...

p-himik 2023-04-21T19:49:20.543509Z

To each their own, of course. But with complex enough data, I just don't see how keyboard interaction can scale. Especially when that data is heavily skewed to be represented graphically.

djblue 2023-04-21T20:03:06.922039Z

I think custom shortcuts for custom commands might make things a little more ergonomic

👍 1
p-himik 2023-04-21T19:37:57.072309Z

A completely separate thing that just made me think I was losing my mind. Why does the pr-str viewer display double 1.0 as 1? Or any other viewer, at that. Clojure makes it a very big deal, and (= 1.0 1) return false. I was going nuts trying to figure out why some pure functions worked on statically provided data but failed on some data provided from some other places - data that has doubles without the fractional part.

seancorfield 2023-04-21T19:41:08.727029Z

Clojure vs ClojureScript.

seancorfield 2023-04-21T19:41:14.180449Z

Or rather JavaScript 🙂

seancorfield 2023-04-21T19:41:32.707209Z

If you're in UI land, you're in JS land, right?

djblue 2023-04-21T19:42:01.003469Z

Yeah, although I though I made sure to encode types for number during serialization. Not sure what's up here, but it is a bug 😢

djblue 2023-04-21T19:43:12.708879Z

Yeah, this is probably due to going from clj -> cljs -> clj

djblue 2023-04-21T19:45:31.154109Z

This is what I get for using json

p-himik 2023-04-21T19:46:49.263839Z

Blimey. I failed to internalize that the UI part is JS. Not sure how I would've fixed it then, apart from creating a representation on the CLJ side. No clue how feasible that is. You would get the same issue anywhere, be it EDN or Transit or msgpack. In JS 1.0 is 1. Unless you assign each numeric value a tag or create representations on the CLJ side, I don't think there's a way around it.

djblue 2023-04-21T19:48:59.863389Z

Yeah, which would then break the js plotting libs that expect numbers 💀

djblue 2023-04-21T19:49:19.671249Z

This is why we can't have nice things!!!

djblue 2023-04-21T19:50:48.929289Z

If you put the number in a container [1.0] I think it will preserve the type since the vector in the runtime is exactly the same one you sent to portal

p-himik 2023-04-21T19:51:20.829929Z

There's another potential alternative. To send the types along with the values, but nested under a separate structure. So you'd have your {:a 1 :b [1 2 3]} but also {:a Long :b [Double Double Long]} or something like that that would be reasonably useful and flexible.

djblue 2023-04-21T19:51:23.602809Z

However, it will still render incorrectly

p-himik 2023-04-21T19:52:10.219689Z

Well, the vector will be a vector, true. But longs and doubles will all become js/Number, right? So it will still be [1]. My numbers were in a map - that didn't help them. :)

djblue 2023-04-21T19:53:16.506079Z

I mean if you pull the value back into the repl, your code should get the correct types

p-himik 2023-04-21T19:54:32.555349Z

Ah, right, yeah.

djblue 2023-04-21T19:56:06.577649Z

I could see a world where you could opt into :preserve-doubles per portal session and use the tagged approach you mentioned above

djblue 2023-04-21T19:56:39.492999Z

https://github.com/djblue/portal/blob/master/src/portal/runtime/cson.cljc#L706 already has something special for the CLR