commit 5930ffd3577d066978c8ea39fa20bf3176776773 Author: bluepython508 <16466646+bluepython508@users.noreply.github.com> Date: Tue Feb 10 10:55:22 2026 +0000 Initial Commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd21df5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.direnv/ +/.jpm/ +/build/ +/result/ +/_build/ diff --git a/bundle/info.jdn b/bundle/info.jdn new file mode 100644 index 0000000..05d122b --- /dev/null +++ b/bundle/info.jdn @@ -0,0 +1 @@ +@{:name "censtablo" :description "River Window Manager" :jpm-dependencies @["https://codeberg.org/ifreund/janet-wayland" "https://codeberg.org/ifreund/janet-xkbcommon"]} diff --git a/bundle/init.janet b/bundle/init.janet new file mode 100644 index 0000000..2989886 --- /dev/null +++ b/bundle/init.janet @@ -0,0 +1,4 @@ +(if (dyn :install-time-syspath) + (use @install-time-syspath/spork/declare-cc) + (use spork/declare-cc)) +(dofile "project.janet" :env (jpm-shim-env)) \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..4c0027d --- /dev/null +++ b/flake.lock @@ -0,0 +1,44 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1767950769, + "narHash": "sha256-oT4Tj7O9361bmMbPwuAcH2zgj2fUZao7F32Bkah+AmE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3cf525869eaad0c4105795523d158d6985d40885", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "river": { + "flake": false, + "locked": { + "lastModified": 1770134013, + "narHash": "sha256-O0GySsnOhbZBi86RdG1U/9ov8llLS/15ZZZVlSQqAv4=", + "ref": "main", + "rev": "e967499fb12b6074ebb93d98d7fc3e5eefb438ab", + "revCount": 1578, + "type": "git", + "url": "https://codeberg.org/river/river.git" + }, + "original": { + "ref": "main", + "type": "git", + "url": "https://codeberg.org/river/river.git" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "river": "river" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..89ff708 --- /dev/null +++ b/flake.nix @@ -0,0 +1,125 @@ +{ + inputs.nixpkgs.url = "github:NixOS/nixpkgs"; + inputs.river = { + url = "git+https://codeberg.org/river/river.git?ref=main"; + flake = false; + }; + + outputs = { + self, + nixpkgs, + river, + }: let + systems = ["x86_64-linux" "aarch64-linux"]; + eachSystem = f: + nixpkgs.lib.genAttrs systems (system: + f { + inherit system; + pkgs = nixpkgs.legacyPackages.${system}; + }); + in { + devShells = eachSystem ({ + pkgs, + system, + ... + }: { + default = pkgs.mkShell (let + libxkbcommon = pkgs.libxkbcommon.overrideAttrs { + version = "1.12.0"; + src = pkgs.fetchFromGitHub { + owner = "xkbcommon"; + repo = "libxkbcommon"; + tag = "xkbcommon-1.12.0"; + hash = "sha256-QO3snl7NiyS2ao2MF3eT/lkNmVBjijr3JdTyrPn/2MQ="; + }; + patches = []; + doCheck = false; + }; + in { + packages = let + janet-pm = pkgs.stdenv.mkDerivation rec { + pname = "janet-pm"; + version = "1.1.1"; + src = pkgs.fetchFromGitHub { + owner = "janet-lang"; + repo = "spork"; + rev = "v${version}"; + hash = "sha256-0L2G7flzNzi8w1nkGcQrtKjGl/D2uC8xSRgsBuLgRxM="; + }; + patches = [ + (pkgs.fetchpatch { + url = "https://github.com/tw4452852/spork/commit/304ff781565d9bac21bef450770a312182730727.patch"; + hash = "sha256-9hTY3s3GphAg07we7gOF8vAPAYaWOjVkaspt8wP5KOU="; + }) + (pkgs.fetchpatch { + url = "https://patch-diff.githubusercontent.com/raw/janet-lang/spork/pull/269.patch"; + hash = "sha256-HMvqmiZR+a2yE54PqlLji2Vi6zoUcQ8eD5HFcqAPxEo="; + }) + ]; + nativeBuildInputs = [pkgs.janet]; + + buildPhase = '' + export JANET_PATH=$out + mkdir $JANET_PATH + janet --install . + ''; + }; + river-pkg = pkgs.stdenv.mkDerivation (finalAttrs: { + name = "river"; + version = "0.4"; + src = river; + deps = pkgs.callPackage ./river.build.zig.zon.nix {}; + zigBuildFlags = ["--system" "${finalAttrs.deps}" "-Dxwayland"]; + nativeBuildInputs = with pkgs; [pkg-config wayland-scanner xwayland zig_0_15.hook]; + buildInputs = with pkgs; [ + libGL + libevdev + libinput + pixman + udev + wayland + wayland-protocols + wlroots_0_19 + libx11 + libxkbcommon + ]; + + dontConfigure = true; + }); + repl = pkgs.writeScriptBin "censtablo-repl" '' + #!/usr/bin/env janet + (import spork/netrepl) + + (netrepl/client :unix (string (os/getenv "XDG_RUNTIME_DIR") "/censtablo-" (os/getenv "WAYLAND_DISPLAY"))) + ''; + in [pkgs.janet janet-pm river-pkg repl]; + + nativeBuildInputs = [pkgs.pkg-config]; + buildInputs = [ + (pkgs.wayland.overrideAttrs (final: prev: { + patches = + prev.patches + ++ [ + (pkgs.fetchpatch { + url = "https://gitlab.freedesktop.org/wayland/wayland/-/merge_requests/485.patch"; + hash = "sha256-gd5BRE/ioFMM86Q8HLSzdvuwDDaqws0b3HiInC0nNgQ="; + }) + ]; + })) + pkgs.wayland-scanner + pkgs.wayland-protocols + libxkbcommon + ]; + + RIVER_PROTOCOLS = "${river}/protocol/"; + + shellHook = '' + export JANET_PATH="$PWD/.jpm" + mkdir -p $JANET_PATH + # Put this at the end so that installing spork in a local environment _doesn't_ override the janet-pm we're providing with patches so that pkg-config doesn't get `with-path` + export PATH="$PATH:$JANET_PATH/bin/" + ''; + }); + }); + }; +} diff --git a/main.janet b/main.janet new file mode 100644 index 0000000..91e924f --- /dev/null +++ b/main.janet @@ -0,0 +1,684 @@ +(import wayland) +(import spork/netrepl) +(import xkbcommon) + +(def river-protocols ((os/environ) "RIVER_PROTOCOLS")) + +(def interfaces (wayland/scan + :system-protocols ["stable/viewporter/viewporter.xml"] + :custom-protocols (map |(string river-protocols $) + ["river-window-management-v1.xml" + "river-layer-shell-v1.xml" + "river-xkb-bindings-v1.xml" + "river-input-management-v1.xml" + "river-libinput-config-v1.xml" + "river-xkb-config-v1.xml"]))) + +(def required-interfaces + {"river_window_manager_v1" 3 + "river_layer_shell_v1" 1 + "river_xkb_bindings_v1" 1 + "river_xkb_config_v1" 1 + "river_input_manager_v1" 1}) + +(def registry @{}) +(def wl-outputs @{}) + +(def wm @{:outputs @[] :seats @[] :windows @[] :render-order @[] :inputs @[] :pulldowns @{}}) + +(def tags-main [:a :r :s :t :n :e :i :o]) +(def tags-offload [:comma :period]) +(def tags-other [:g :m :q :w :f :p :b :j :l :u :y :z :x :c :d :v :k :h]) + +(def config + @{:xcursor-theme ["Vanilla-DMZ" 24] + :border-width 1 + :border-normal 0x646464 + :border-focused 0xffffff + :appid-rules @{}}) + +(defn binding/create [seat keysym mods action] + (def binding @{:obj (:get-xkb-binding (registry "river_xkb_bindings_v1") (seat :obj) (xkbcommon/keysym keysym) mods)}) + (defn handle-event [event] + (match event + [:pressed] (put seat :pending-action [binding action]))) + (:set-handler (binding :obj) handle-event) + (:enable (binding :obj)) + (array/push (seat :xkb-bindings) binding)) + +(defn input/configure [input] + # Map trackpads to laptop displays + (unless (nil? (string/find "Touchpad" (input :name))) + (when-let [internal (find |(string/has-prefix? "eDP" (get $ :name "")) wl-outputs)] + (put input :mapped internal) + (:map-to-output (input :obj) (internal :obj))))) + +(defn input/create [input] + (def input @{:obj input}) + (defn handle-event [event] + (match event + [:type type] (put input :type type) + [:name name] (put input :name name)) + (when (and (input :type) (input :name)) + (input/configure input))) + (:set-handler (input :obj) handle-event) + (:set-user-data (input :obj) input) + (array/push (wm :inputs) input)) + +(defn wl-output/create [name obj] + (def output @{:obj obj}) + (put wl-outputs name output) + (defn handle-event [event] + (match event + [:name name] (do + (put output :name name) + (each input (wm :inputs) (input/configure input))))) + (:set-handler obj handle-event) + (:set-user-data obj output)) + +(defn output/create [obj] + (def output @{:obj obj + :layer-shell (:get-output (registry "river_layer_shell_v1") obj) + :new true + :tags-other []}) + (defn handle-event [event] + (match event + [:removed] (put output :removed true) + [:position x y] (put output :position [x y]) + [:dimensions w h] (put output :dimensions [w h]) + [:wl-output name] (put output :wl-output (wl-outputs name)))) + (defn handle-layer-shell-event [event] + (match event + [:non-exclusive-area x y w h] (put output :non-exclusive-area [x y w h]))) + (:set-user-data obj output) + (:set-handler obj handle-event) + (:set-handler (output :layer-shell) handle-layer-shell-event) + output) + +(defn seat/create [obj] + (def seat @{:obj obj + :layer-shell (:get-seat (registry "river_layer_shell_v1") obj) + :layer-focus :none + :xkb-bindings @[] + :pointer-bindings @[] + :new true}) + (defn handle-event [event] + (match event + [:removed] (put seat :removed true) + [:pointer-enter window] (put seat :pointer-target (:get-user-data window)) + [:pointer-leave] (put seat :pointer-target nil) + [:window-interaction window] (put seat :window-interaction (:get-user-data window)) + [:shell-surface-interaction shell-surface] (do))) + (defn handle-layer-shell-event [event] + (match event + [:focus-exclusive] (put seat :layer-focus :exclusive) + [:focus-non-exclusive] (put seat :layer-focus :non-exclusive) + [:focus-none] (put seat :layer-focus :none))) + (:set-handler obj handle-event) + (:set-handler (seat :layer-shell) handle-layer-shell-event) + (:set-user-data obj seat) + (:set-xcursor-theme obj ;(config :xcursor-theme)) + seat) + +(defn window/create [obj] + (def window + @{:obj obj + :node (:get-node obj) + :new true}) + (defn handle-event [event] + (match event + [:closed] (put window :closed true) + [:dimensions-hint min-w min-h max-w max-h] (put window :dimensions-bounds {:min-w min-w :max-w max-w :min-h min-h :max-h max-h}) + [:dimensions w h] (do (put window :w w) (put window :h h)) + [:app-id app-id] (put window :app-id app-id) + [:parent parent] (put window :parent (if parent (:get-user-data parent))) + [:decoration-hint hint] (put window :decoration-hint hint) + [:pointer-move-requested seat] (put window :pointer-move-requested {:seat (:get-user-data seat)}) + [:pointer-resize-requested seat edges] (put window :pointer-resize-requested {:seat (:get-user-data seat) :edges edges}) + [:fullscreen-requested output] (put window :fullscreen-requested [:enter (if output (:get-user-data output))]) + [:exit-fullscreen-requested] (put window :fullscreen-requested [:exit]))) + (:set-handler obj handle-event) + (:set-user-data obj window) + window) + +(defn clear [keys] + (fn [obj] + (each k keys + (put obj k nil)))) + +(defn output/set-main-tag [output tag] + (map |(when (= ($ :tag-main) tag) (put $ :tag-main nil)) (wm :outputs)) + (put output :tag-main tag) + (put output :tag-offload nil)) + +(defn output/toggle-other-tag [output tag] + (if (has-value? (output :tags-other) tag) + (do + (put output :tags-other (filter |(not (= $ tag)) (output :tags-other))) + false) + (do + (map (fn [output] (update output :tags-other (fn [tags] (filter |(not (= $ tag)) tags)))) (wm :outputs)) + (update output :tags-other |[;$ tag]) + true))) + +(defn output/toggle-offload-tag [output tag] + (put output :tag-offload (if (= (output :tag-offload) tag) nil tag))) + +(defn output/ensure-main-tag [output] + (when (not (output :tag-main)) + (def available (->> tags-main + (reverse) + (filter (fn [tag] (not (some |(= tag ($ :tag-main)) (wm :outputs))))))) + (def tag (or + (find (fn [tag] (not (some |(= ($ :tag) tag) (wm :windows)))) available) + (first available))) + (put output :tag-main tag))) + +(defn output/manage [output] + (put output :tags [(output :tag-main) + ;(output :tags-other) + ;(if (output :tag-offload) + [[(output :tag-main) (output :tag-offload)]] + [])])) + +(defn window/enter-fullscreen [window &opt inform] + (put window :fullscreen true) + (when inform + (:inform-fullscreen (window :obj))) + (:fullscreen (window :obj) ((window :output) :obj))) + +(defn window/exit-fullscreen [window &opt inform] + (put window :fullscreen false) + (when inform + (:inform-not-fullscreen (window :obj))) + (:exit-fullscreen (window :obj))) + +(defn window/set-float [window float] + (if float + (:set-tiled (window :obj) {}) + (:set-tiled (window :obj) {:left true :bottom true :top true :right true})) + (put window :float float)) + +(defn window/move-output [window output] + (when output + (put window :tag (output :tag-main)) + (put window :output output))) + +(defn seat/focus-output [seat output] + (unless (= output (seat :focused-output)) + (put seat :focused-output output) + (when output (:set-default (output :layer-shell))))) + +(defn seat/focus [seat window] + (defn focus-window [window] + (unless (= (seat :focused) window) + (:focus-window (seat :obj) (window :obj)) + (put seat :focused window) + (set (wm :render-order) (filter |(not (= $ window)) (wm :render-order))) + (array/push (wm :render-order) window) + (:place-top (window :node)))) + (defn clear-focus [] + (when (seat :focused) + (:clear-focus (seat :obj)) + (put seat :focused nil))) + (defn focus-non-layer [] + (when (and window (window :output)) + (seat/focus-output seat (window :output))) + (when-let [output (seat :focused-output)] + (defn visible? [w] (and w (= output (w :output)))) + (def visible (filter |(= output ($ :output)) (wm :render-order))) + (cond + (def fullscreen (last (filter |($ :fullscreen) visible))) (focus-window fullscreen) + (visible? window) (focus-window window) + (visible? (seat :focused)) (do) + (def top-visible (last visible)) (focus-window top-visible) + (clear-focus)))) + (case (seat :layer-focus) + :exclusive (put seat :focused nil) + :non-exclusive (if window + (do + (put seat :layer-focus :none) + (focus-non-layer)) + (put seat :focused nil)) + :none (focus-non-layer))) + +(defn window/manage [window] + (when (window :new) + (:use-ssd (window :obj)) + (if-let [parent (window :parent)] + (do + (window/set-float window true) + (put window :tag (parent :tag)) + (:propose-dimensions (window :obj) 0 0)) + (do + (def seat (first (wm :seats))) + (window/set-float window false) + (window/move-output window (seat :focused-output)) + (each seat (wm :seats) (seat/focus seat window)))) + (when-let [rule ((config :appid-rules) (window :app-id))] + (rule window))) + + (match (window :fullscreen-requested) + [:enter] (window/enter-fullscreen window) + [:enter output] (do + (window/move-output window output) + (window/enter-fullscreen window)) + [:exit] (window/exit-fullscreen window)) + + (put window :output (find |(has-value? ($ :tags) (window :tag)) (wm :outputs))) + + (if (window :output) + (do + (:show (window :obj)) + (when (window :fullscreen) + (:fullscreen (window :obj) ((window :output) :obj)))) + (:hide (window :obj)))) + + +(defn seat/manage [seat] + (when (seat :new) + (each binding (config :xkb-bindings) + (binding/create seat ;binding))) + + (when-let [window (seat :focused)] + (when (window :closed) + (put seat :focused nil))) + + (if (or (not (seat :focused-output)) ((seat :focused-output) :removed)) + (seat/focus-output seat (first (wm :outputs)))) + + (seat/focus seat nil) + + (if-let [window (seat :window-interaction)] + (seat/focus seat window)) + + (when-let [[binding action] (seat :pending-action)] + (action seat binding)) + + (seat/focus seat nil)) + +(defn window/set-position [window x y] + (let [border-width (config :border-width) + x (+ x border-width) + y (+ y border-width)] + (put window :x x) + (put window :y y) + (:set-position (window :node) x y))) + +(defn window/propose-dimensions [window w h] + (:propose-dimensions (window :obj) + (max 1 (- w (* 2 (config :border-width)))) + (max 1 (- h (* 2 (config :border-width)))))) + +(defn layout/rows [n x y w h] + (if (> n 0) + (do + (def height (/ h n)) + (defn rows [n] + (if (= n 0) [] (let [Y (- (+ y h) (* n height))] + (tuple/join [[x Y w height]] + (rows (- n 1)))))) + (rows n)) + [])) + +(defn layout/split [count n x y w h] + (if (= n 1) + [[x y w h]] + (let [left (/ w 2) + right (- w left)] + (tuple/join (layout/rows (min n count) x y (+ x left) h) + (layout/rows (- n count) (+ x left) y right h))))) + +(defn layout/main [n w h] (layout/split 1 n 0 0 w h)) + +(defn output/windows [output] (filter |(= output ($ :output)) (wm :windows))) + +(defn output/layout [output] + (def [X Y W H] (or (output :non-exclusive-area) [;(output :position) ;(output :dimensions)])) + (def windows (filter |(not ($ :float)) (output/windows output))) + (map (fn [window [x y w h]] + (window/set-position window (+ X x) (+ Y y)) + (window/propose-dimensions window w h)) + windows + (layout/main (length windows) W H)) + + (each window (filter |($ :float) (output/windows output)) + (let [t (+ Y (/ H 4)) + l (+ X (/ W 4)) + h (/ H 2) + w (/ W 2)] + (window/set-position window l t) + (window/propose-dimensions window w h) + (:place-top (window :node))))) + +(defn wm/manage [] + (set (wm :render-order) (filter |(not ($ :closed)) (wm :render-order))) + (defn close [o] (not (when (or (o :removed) (o :closed)) (:destroy (o :obj)) true))) + (set (wm :outputs) (filter close (wm :outputs))) + (set (wm :windows) (filter close (wm :windows))) + (set (wm :seats) (filter close (wm :seats))) + + (defn output/set-main-tag [output tag] + (map |(if (= ($ :tag-main)) tag) (wm :outputs))) + (map seat/manage (wm :seats)) + (map output/ensure-main-tag (wm :outputs)) + (map output/manage (wm :outputs)) + (map window/manage (wm :windows)) + (map |(seat/focus $ nil) (wm :seats)) # Reconcile focus again after potential tag changes + + (map output/layout (wm :outputs)) + + (map (clear [:new]) (wm :outputs)) + (map (clear [:new :pointer-move-requested :pointer-resize-requested :fullscreen-requested]) (wm :windows)) + (map (clear [:new :window-interaction :pending-action]) (wm :seats)) + + (:manage-finish (registry "river_window_manager_v1"))) + +(defn rgb-to-u32-rgba [rgb] + [(* (band 0xff (brushift rgb 16)) (/ 0xffff_ffff 0xff)) + (* (band 0xff (brushift rgb 8)) (/ 0xffff_ffff 0xff)) + (* (band 0xff rgb) (/ 0xffff_ffff 0xff)) + 0xffff_ffff]) + +(defn set-borders [window status] + (def rgb (case status + :normal (config :border-normal) + :focused (config :border-focused))) + (:set-borders (window :obj) + {:left true :bottom :true :top :true :right true} + (config :border-width) + ;(rgb-to-u32-rgba rgb))) + +(defn window/render [window] + (when (and (not (window :x)) (window :w)) + (def output ((window :parent) :output)) + (window/set-position + window + (+ (output :x) (div (- (output :w) (window :w)) 2)) + (+ (output :y) (div (- (output :h) (window :h)) 2)))) + (if (find |(= ($ :focused) window) (wm :seats)) + (set-borders window :focused) + (set-borders window :normal))) + +(defn wm/render [] + (map window/render (wm :windows)) + (:render-finish (registry "river_window_manager_v1"))) + + +(defn wm/handle-event [event] + (match event + [:unavailable] (error "another window manager is already running") + [:finished] (os/exit 0) + [:manage-start] (wm/manage) + [:render-start] (wm/render) + [:output obj] (array/push (wm :outputs) (output/create obj)) + [:seat obj] (array/push (wm :seats) (seat/create obj)) + [:window obj] (array/push (wm :windows) (window/create obj)))) + +(defn mark-dirty [] + (:manage-dirty (registry "river_window_manager_v1"))) + + +(defn action/target-in [obj list dir] + (let [i (or (index-of obj list) -1)] + (case dir + :next (get list (+ i 1) (first list)) + :prev (get list (- i 1) (last list))))) + +(defn action/target [seat dir] + (when-let [window (seat :focused) + output (window :output) + windows (output/windows output)] + (action/target-in window windows dir))) + +(defn spawn [command] + (ev/spawn (os/proc-wait (os/spawn ["/bin/sh" "-c" command] :p)))) + +(defn action/spawn [command] + (fn [seat binding] (spawn command))) + +(defn action/focus [dir] (fn [seat binding] (seat/focus seat (action/target seat dir)))) +(defn action/focus-output [dir] + (fn [seat binding] + (when-let [current-output (seat :focused-output) + outputs (wm :outputs) + new-output (action/target-in current-output outputs dir)] + (seat/focus-output seat new-output) + (seat/focus seat (first (output/windows new-output)))))) + +(defn action/goto-tag [tag] + (def f (cond + (has-value? tags-main tag) output/set-main-tag + (has-value? tags-other tag) output/toggle-other-tag + (has-value? tags-offload tag) output/toggle-offload-tag)) + (fn [seat binding] + (when-let [output (seat :focused-output)] + (f output tag)))) + +(defn action/set-tag [tag] + (fn [seat binding] + (when-let [window (seat :focused) + output (seat :focused-output) + main-tag (output :tag-main)] + (put window :tag (cond + (= (window :tag) [main-tag tag]) main-tag + (has-value? tags-offload tag) [main-tag tag] + tag))))) + +(defn action/toggle-fullscreen [inform] + (fn [seat binding] + (def window (seat :focused)) + (if (window :fullscreen) + (window/exit-fullscreen window inform) + (window/enter-fullscreen window inform)))) + +(defn action/close [] + (fn [seat binding] + (if-let [window (seat :focused)] + (:close (window :obj))))) + +(defn action/zoom [] + (fn [seat binding] + (when-let [focused (seat :focused) + output (focused :output) + visible (output/windows output) + target (if (= focused (first visible)) (get visible 1) focused) + i (assert (index-of target (wm :windows)))] + (array/remove (wm :windows) i) + (array/insert (wm :windows) 0 target) + (seat/focus seat (first (wm :windows)))))) + +(defn action/screenshot [] + (fn [seat binding] + (spawn (string "GRIM_DEFAULT_DIR=\"$HOME/tmp\" grim -o " (get-in seat [:focused-output :wl-output :name]))))) + +(defn action/rotate-outputs [] + (def ks [:tag-main :tag-offload :tags-other]) + (fn [seat binding] + (def outputs (wm :outputs)) + (def tags (map |(table (seq [k :in ks] k ($ k))) outputs)) + (map (fn [out tags] + (eachp [k tag] tags (put out k tag))) outputs [;(slice tags 1) (first tags)]))) + +(defn rule/pulldown [] + (fn [window] + (def appid (window :app-id)) + (put window :tag [:pulldown appid]) + (window/set-float window true) + (when-let [w ((wm :pulldowns) appid)] + (:close (w :obj))) + (put (wm :pulldowns) appid window))) + +(defn action/pulldown [appid command] + (fn [seat binding] + (when (output/toggle-other-tag (seat :focused-output) [:pulldown appid]) + (if-let [window (get-in wm [:pulldowns appid])] + (if-not (window :closed) + (do + (put window :output (seat :focused-output)) + (seat/focus seat window)) + (spawn command)) + (spawn command))))) + + +(defn locked-screen [] + (print "Locked!") + (def notifs-status (= 0 (os/execute ["dunstctl" "is-paused" "-e"] :p))) + (os/execute ["dunstctl" "set-paused" "true"] :p) + (os/execute ["pidwait" "swaylock"] :p) # Blocking is fine here -- we're locked + (os/execute ["dunstctl" "set-paused" (if notifs-status "true" "false")] :p)) + +(defn lock-screen [] + (spawn "swaylock") + (locked-screen)) + +(defn action/lock-screen [] + (fn [seat binding] + (lock-screen))) + +(put config :xkb-bindings (let [G {:mod4 true} + G-S {:mod4 true :shift true} + M {:mod1 true} + M-S {:mod1 true :shift true}] + [[:Tab M (action/focus :next)] + [:Tab M-S (action/focus :prev)] + [:Tab G (action/focus-output :next)] + [:Tab G-S (action/focus-output :prev)] + [:Tab {:mod4 true :ctrl true :mod1 true} (action/spawn "toggle-monitor")] + [:Return G (action/spawn "alacritty")] + [:f G (action/toggle-fullscreen true)] + [:f G-S (action/toggle-fullscreen false)] + [:k G (action/close)] + [:backslash G (action/zoom)] + [:backslash G-S (action/rotate-outputs)] + [:space G (action/spawn "rofi -show launch")] + [:x {:mod4 true} (action/spawn "rofi -show run")] + [:b G (action/spawn "rofi -show bt")] + [:p G (action/spawn "rofi-rbw")] + [:n G (action/spawn "rofi -show notifications")] + [:m G (action/spawn "rofi -show cliphist")] + [:s G (action/screenshot)] + [:j G (action/spawn "io.github.alainm23.planify.quick-add")] + [:o G (action/spawn "wl-present-ui")] + [:t G (action/pulldown "floating-terminal" "alacritty --class floating-terminal -e tmux new-session -ADX -s floating")] + [:r G (action/pulldown "floating-repl" "alacritty --class floating-repl -e censtablo-repl")] + [:XF86AudioLowerVolume {} (action/spawn "pamixer -d 5")] + [:XF86AudioRaiseVolume {} (action/spawn "pamixer -i 5")] + [:XF86AudioMute {} (action/spawn "pamixer -t")] + [:XF86AudioPlay {} (action/spawn "playerctl play-pause")] + [:XF86AudioPrev {} (action/spawn "playerctl previous")] + [:XF86AudioNext {} (action/spawn "playerctl next")] + [:Pause {} (action/lock-screen)] + ;(map |[$ {:mod5 true} (action/goto-tag $)] (tuple ;tags-main ;tags-other ;tags-offload)) + ;(map |[$ {:mod5 true :shift true} (action/set-tag $)] (tuple ;tags-main ;tags-other ;tags-offload))])) + + +(defn rule/tag [tag] (fn [window] (put window :tag tag))) + +(put config :appid-rules @{"thunderbird" (rule/tag :m) + "vesktop" (rule/tag :d) + "io.github.alainm23.planify" (rule/tag :j) + "floating-terminal" (rule/pulldown) + "floating-repl" (rule/pulldown)}) + + +(defn startup [] + (spawn "pkill waybar && waybar") + (spawn "kanshi") + + (spawn "vesktop") + (spawn "thunderbird") + (spawn "solaar --window hide") + (spawn "io.github.alainm23.planify")) + +# TODO: layout flexibility + +# TODO: swap display tags + +# TODO: reload with repl? + +(defn configure-keymap [] + (def ctx (xkbcommon/context/new)) + (def rmlvo (:create-rmlvo ctx)) + (:append-layout rmlvo "us-local") + (def keymap (:create-keymap rmlvo :text-v2)) + (def str (:get-string keymap :text-v2)) + (def fd (wayland/memfd/from-string str)) + + (def keymap (:create-keymap (registry "river_xkb_config_v1") fd :text-v2)) + (defn apply-km [kb] + (:set-keymap kb keymap) + (:set-layout-by-name kb "us-local")) + (var ready false) + (def keyboards @[]) + (:set-handler keymap (fn [event] + (match event + [:success] (do + (set ready true) + (each keyboard keyboards (apply-km keyboard))) + [:failure err] (errorf "invalid keymap %s" err)))) + + (:set-handler (registry "river_xkb_config_v1") (fn [event] + (match event + [:xkb-keyboard keyboard] (if ready (apply-km keyboard) (array/push keyboards keyboard)))))) + + +(def repl-env (curenv)) +(defn repl-server-create [] + (def path (string/format "%s/censtablo-%s" (os/getenv "XDG_RUNTIME_DIR") (os/getenv "WAYLAND_DISPLAY"))) + (protect (os/rm path)) + (netrepl/server :unix path repl-env)) + +# Stolen from https://git.sr.ht/~leon_plickat/river-config/tree/master/item/river/launch-river.sh +(def env {"WAYLAND_DEBUG" nil + "MOZ_ENABLE_WAYLAND" "1" + "CLUTTER_BACKEND" "wayland" + "QT_QPA_PLATFORM" "wayland" + "ECORE_EVAS_ENGINE" "wayland-egl" + "ELM_ENGINE" "wayland_egl" + "_JAVA_AWT_WM_NONREPARENTING" "1" + "NO_AT_BRIDGE" "1" + + "XDG_SESSION_TYPE" "wayland" + "XDG_SESSION_DESKTOP" "river" + "XDG_CURRENT_DESKTOP" "river" + + "NIXOS_OZONE_WL" "1"}) + +(defn main [&] + (def display (wayland/connect interfaces)) + (eachp [var val] env (os/setenv var val)) + + (put registry :obj (:get-registry display)) + (:set-handler (registry :obj) (fn [event] + (match event + [:global name interface version] + (do + (when-let [required-version (required-interfaces interface)] + (when (< version required-version) + (errorf "wayland compositor supported %s version too old (need %d, got %d)" interface required-version version)) + (put registry interface (:bind (registry :obj) name interface required-version))) + (when (= interface "wl_output") + (wl-output/create name (:bind (registry :obj) name interface 4)))) + [:global-remove name] + (when-let [obj (wl-outputs name)] + (put wl-outputs name nil) + (:release (obj :obj)))))) + (:roundtrip display) + (eachk i required-interfaces + (unless (get registry i) + (errorf "wayland compositor does not support %s" i))) + + (:set-handler (registry "river_window_manager_v1") wm/handle-event) + (:set-handler (registry "river_input_manager_v1") (fn [event] + (match event + [:input-device input] (input/create input)))) + (configure-keymap) + + (:roundtrip display) + + (startup) + + (def repl-server (repl-server-create)) + (defer (:close repl-server) + (forever (:dispatch display)))) diff --git a/project.janet b/project.janet new file mode 100644 index 0000000..61b264b --- /dev/null +++ b/project.janet @@ -0,0 +1,9 @@ +(declare-project + :name "censtablo" + :description "" + :dependencies @["spork" "https://codeberg.org/ifreund/janet-wayland"] + :version "0.0.0") + +(declare-executable + :name "censtablo" + :entry "./main.janet") diff --git a/river.build.zig.zon.nix b/river.build.zig.zon.nix new file mode 100644 index 0000000..6427ec2 --- /dev/null +++ b/river.build.zig.zon.nix @@ -0,0 +1,138 @@ +# generated by zon2nix (https://github.com/Cloudef/zig2nix) + +{ + lib, + linkFarm, + fetchurl, + fetchgit, + runCommandLocal, + zig, + name ? "zig-packages", +}: + +let + unpackZigArtifact = + { name, artifact }: + runCommandLocal name { nativeBuildInputs = [ zig ]; } '' + hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})" + mv "$TMPDIR/p/$hash" "$out" + chmod 755 "$out" + ''; + + fetchZig = + { + name, + url, + hash, + }: + let + artifact = fetchurl { inherit url hash; }; + in + unpackZigArtifact { inherit name artifact; }; + + fetchGitZig = + { + name, + url, + hash, + rev ? throw "rev is required, remove and regenerate the zon2json-lock file", + }: + let + parts = lib.splitString "#" url; + url_base = lib.elemAt parts 0; + url_without_query = lib.elemAt (lib.splitString "?" url_base) 0; + in + fetchgit { + inherit name rev hash; + url = url_without_query; + deepClone = false; + }; + + fetchZigArtifact = + { + name, + url, + hash, + ... + }@args: + let + parts = lib.splitString "://" url; + proto = lib.elemAt parts 0; + path = lib.elemAt parts 1; + fetcher = { + "git+http" = fetchGitZig ( + args + // { + url = "http://${path}"; + } + ); + "git+https" = fetchGitZig ( + args + // { + url = "https://${path}"; + } + ); + http = fetchZig { + inherit name hash; + url = "http://${path}"; + }; + https = fetchZig { + inherit name hash; + url = "https://${path}"; + }; + }; + in + fetcher.${proto}; +in +linkFarm name [ + { + name = "pixman-0.3.0-LClMnz2VAAAs7QSCGwLimV5VUYx0JFnX5xWU6HwtMuDX"; + path = fetchZigArtifact { + name = "pixman"; + url = "https://codeberg.org/ifreund/zig-pixman/archive/v0.3.0.tar.gz"; + hash = "sha256-zX/jQV1NWGhalP3t0wjpmUo38BKCiUDPtgNGHefyxq0="; + }; + } + { + name = "wayland-0.5.0-dev-lQa1kvTUAQCsD8AobfOXJA_-TVG-WTYXju68OZ8L85RM"; + path = fetchZigArtifact { + name = "wayland"; + url = "git+https://codeberg.org/ifreund/zig-wayland?ref=main#f2480d25764a50ed2fe29f49e4209c074a557f46"; + hash = "sha256-PosVlJ0FD80O46l0SYTWzfHFYfIE4Bdjqof/gttQ+KM="; + rev = "f2480d25764a50ed2fe29f49e4209c074a557f46"; + }; + } + { + name = "wlroots-0.19.3-jmOlcuL_AwBHhLCwpFsXbTizE3q9BugFmGX-XIxqcPMc"; + path = fetchZigArtifact { + name = "wlroots"; + url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.19.3.tar.gz"; + hash = "sha256-v/z6Kva5DQfLGHzrPXCu/7DtEbPT+XN+oATnIrwDHWs="; + }; + } + { + name = "wayland-0.4.0-lQa1khbMAQAsLS2eBR7M5lofyEGPIbu2iFDmoz8lPC27"; + path = fetchZigArtifact { + name = "wayland"; + url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.4.0.tar.gz"; + hash = "sha256-kH/dGMft4asfJJxhi6Xc47inLCblAJC0rnAQAzpBm6k="; + }; + } + { + name = "xkbcommon-0.3.0-VDqIe3K9AQB2fG5ZeRcMC9i7kfrp5m2rWgLrmdNn9azr"; + path = fetchZigArtifact { + name = "xkbcommon"; + url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.3.0.tar.gz"; + hash = "sha256-HhhUI+ayPtlylhTmZ1GrdSLbRIffTg3MeisGN1qs2iM="; + }; + } + { + name = "xkbcommon-0.4.0-dev-VDqIe3bUAQB9reSurmSJxQjiQ6bnpekmOxIp_077GhVZ"; + path = fetchZigArtifact { + name = "xkbcommon"; + url = "git+https://codeberg.org/ifreund/zig-xkbcommon?ref=main#a10651157cb1545ee3ae3818b4c7437c64d16462"; + hash = "sha256-fHf99JXSC1duAGoydZerzEGfmYUyZNEHdlz/SHy/xh8="; + rev = "a10651157cb1545ee3ae3818b4c7437c64d16462"; + }; + } +]