Compare commits

5 Commits

Author SHA1 Message Date
a2ed40d348 Update src/lazygitclj/core.clj
SCIP Index / index (push) Successful in 2m38s
2026-02-04 06:16:48 -10:00
65f7d184bc Update src/lazygitclj/core.clj
SCIP Index / index (push) Successful in 2m41s
2026-02-04 06:14:22 -10:00
a7773b77d1 Update src/lazygitclj/core.clj
SCIP Index / index (push) Successful in 7m34s
2026-02-04 05:21:41 -10:00
ca1e90f130 Update src/lazygitclj/core.clj
SCIP Index / index (push) Successful in 3m38s
2026-02-04 05:12:30 -10:00
e63230af71 Update src/lazygitclj/core.clj
SCIP Index / index (push) Successful in 1m40s
2026-02-03 21:51:07 -10:00
6 changed files with 72 additions and 343 deletions
+1 -16
View File
@@ -8,23 +8,10 @@
## Local TUI Library ## Local TUI Library
- The TUI library is at `../clojure-tui/` — use local override for development: - The TUI library is at `../clojure-tui/` (local dependency in bb.edn)
```
bb -Sdeps '{:deps {io.github.ajet/clojure-tui {:local/root "../clojure-tui"}}}' start
```
- `bb.edn` uses a git dep (for bbin install), override with `:local/root` when developing against local TUI changes
- You have access to edit the local TUI library in conjunction with this repo - You have access to edit the local TUI library in conjunction with this repo
- Use this access to debug issues, support new features, and simplify code - Use this access to debug issues, support new features, and simplify code
- Look for opportunities to create better abstraction primitives for TUI layout - Look for opportunities to create better abstraction primitives for TUI layout
- When updating the TUI library, update the `:git/sha` in `bb.edn` after pushing clojure-tui changes
## Installation (bbin)
End users install from Gitea:
```
bbin install https://git.ajet.fyi/ajet/lazygitclj.git
```
This requires the `clojure-tui` git dep SHA in `bb.edn` to be up-to-date and pushed.
## Testing with VHS ## Testing with VHS
@@ -65,11 +52,9 @@ test/
| Command | Purpose | | Command | Purpose |
|---------|---------| |---------|---------|
| `bb start` | Run lazygitclj TUI | | `bb start` | Run lazygitclj TUI |
| `bb -Sdeps '{:deps {io.github.ajet/clojure-tui {:local/root "../clojure-tui"}}}' start` | Run with local TUI lib |
| `bb debug` | Debug TUI layout issues | | `bb debug` | Debug TUI layout issues |
| `bb test` | Run unit tests | | `bb test` | Run unit tests |
| `bb test:e2e` | Run VHS tape tests | | `bb test:e2e` | Run VHS tape tests |
| `bbin install https://git.ajet.fyi/ajet/lazygitclj.git` | Install for end users |
## Architecture: Elm Pattern ## Architecture: Elm Pattern
+1 -3
View File
@@ -1,7 +1,5 @@
{:paths ["src" "test"] {:paths ["src" "test"]
:deps {io.github.ajet/clojure-tui {:git/url "https://git.ajet.fyi/ajet/clojure-tui.git" :deps {io.github.ajet/clojure-tui {:local/root "../clojure-tui"}}
:git/sha "4a051304888d5b4d937262decba919e1a79dd03d"}}
:bbin/bin {lazygitclj {:main-opts ["-m" "lazygitclj.core"]}}
:tasks {:requires ([e2e]) :tasks {:requires ([e2e])
start {:doc "Run lazygitclj" start {:doc "Run lazygitclj"
:task (exec 'lazygitclj.core/-main)} :task (exec 'lazygitclj.core/-main)}
-3
View File
@@ -1,3 +0,0 @@
{:paths ["src"]
:deps {io.github.ajet/clojure-tui {:git/url "https://git.ajet.fyi/ajet/clojure-tui.git"
:git/sha "4a051304888d5b4d937262decba919e1a79dd03d"}}}
View File
+70 -303
View File
@@ -6,6 +6,8 @@
[lazygitclj.git :as git] [lazygitclj.git :as git]
[clojure.string :as str])) [clojure.string :as str]))
(def temp tui/run)
;; === Model === ;; === Model ===
(defn load-git-data [] (defn load-git-data []
@@ -55,47 +57,8 @@
(str (subs s 0 max-len) "...") (str (subs s 0 max-len) "...")
s)) s))
(defn parse-diff-hunks
"Parse a diff string into header lines and hunk boundaries.
Returns {:header [lines] :hunks [{:start idx :end idx :lines [lines]} ...] :lines [all-lines]}"
[diff-text]
(when (and diff-text (not (str/blank? diff-text)))
(let [lines (vec (str/split-lines diff-text))
hunk-starts (vec (keep-indexed
(fn [i line] (when (str/starts-with? line "@@") i))
lines))]
(when (seq hunk-starts)
(let [header-end (first hunk-starts)
header (subvec lines 0 header-end)
hunks (mapv (fn [i]
(let [start (nth hunk-starts i)
end (if (< (inc i) (count hunk-starts))
(nth hunk-starts (inc i))
(count lines))]
{:start start :end end :lines (subvec lines start end)}))
(range (count hunk-starts)))]
{:header header :hunks hunks :lines lines})))))
(defn hunk-to-patch
"Construct a valid git patch from parsed diff header and a single hunk."
[parsed-diff hunk-idx]
(let [hunk (get (:hunks parsed-diff) hunk-idx)]
(when hunk
(str (str/join "\n" (:header parsed-diff)) "\n"
(str/join "\n" (:lines hunk)) "\n"))))
(defn line->hunk-index
"Find which hunk index contains the given line index, or nil if in header."
[parsed-diff line-idx]
(when parsed-diff
(first (keep-indexed
(fn [i hunk]
(when (and (>= line-idx (:start hunk)) (< line-idx (:end hunk)))
i))
(:hunks parsed-diff)))))
(defn get-current-diff (defn get-current-diff
"Get the diff for the currently selected item based on panel and cursor." "Change doc string for test"
[model] [model]
(let [panel (:panel model) (let [panel (:panel model)
items (current-items model) items (current-items model)
@@ -129,7 +92,8 @@
;; === Update === ;; === Update ===
(defn update-diff [model] (defn update-diff [model]
(assoc model :diff (get-current-diff model))) ;; never update lol
model)
(defn initial-model [] (defn initial-model []
(let [base (merge (let [base (merge
@@ -142,12 +106,6 @@
:menu-mode nil :menu-mode nil
:commits-tab :commits :commits-tab :commits
:branches-tab :local :branches-tab :local
:diff-focused false
:diff-scroll 0
:diff-staging false
:diff-hunks nil
:diff-cursor 0
:diff-file-item nil
:reflog-index 0} :reflog-index 0}
(load-git-data))] (load-git-data))]
(update-diff base))) (update-diff base)))
@@ -201,22 +159,6 @@
(key= event \s #{:shift}) (key= event \s #{:shift})
{:model (assoc model :menu-mode :stash-options)} {:model (assoc model :menu-mode :stash-options)}
;; Enter: interactive staging view
(key= event :enter)
(if item
(let [parsed (when (:diff model) (parse-diff-hunks (:diff model)))]
(if (and parsed (seq (:hunks parsed)))
{:model (assoc model
:diff-focused true
:diff-staging true
:diff-hunks parsed
:diff-cursor (:start (first (:hunks parsed)))
:diff-file-item item)}
(if (:diff model)
{:model (assoc model :diff-focused true :diff-scroll 0)}
{:model model})))
{:model model})
:else :else
{:model model}))) {:model model})))
@@ -270,12 +212,6 @@
{:model (assoc model :message (str "SHA: " sha " (not copied - no clipboard support)"))} {:model (assoc model :message (str "SHA: " sha " (not copied - no clipboard support)"))}
{:model model}) {:model model})
;; Enter: focus diff view
(key= event :enter)
(if (some? (:diff model))
{:model (assoc model :diff-focused true :diff-scroll 0)}
{:model model})
:else :else
{:model model}))) {:model model})))
@@ -298,9 +234,7 @@
(git/checkout-tag item) (git/checkout-tag item)
{:model (-> model refresh (assoc :message (str "Checked out tag " item)))}) {:model (-> model refresh (assoc :message (str "Checked out tag " item)))})
:else (if (some? (:diff model)) :else {:model model})
{:model (assoc model :diff-focused true :diff-scroll 0)}
{:model model}))
;; n: new branch (local tab only) ;; n: new branch (local tab only)
(and (key= event \n) (= tab :local)) (and (key= event \n) (= tab :local))
@@ -378,12 +312,6 @@
(and (key= event \n) ref) (and (key= event \n) ref)
{:model (assoc model :input-mode :stash-branch :input-buffer "" :input-context ref)} {:model (assoc model :input-mode :stash-branch :input-buffer "" :input-context ref)}
;; Enter: focus diff view
(key= event :enter)
(if (some? (:diff model))
{:model (assoc model :diff-focused true :diff-scroll 0)}
{:model model})
:else :else
{:model model}))) {:model model})))
@@ -566,84 +494,6 @@
(:input-mode model) (:input-mode model)
(update-input-mode ctx) (update-input-mode ctx)
;; Diff focused mode
(:diff-focused model)
(if (:diff-staging model)
;; Interactive staging mode: line-by-line navigation + hunk staging
(cond
(key= event :escape)
{:model (-> model
(assoc :diff-focused false :diff-staging false
:diff-hunks nil :diff-file-item nil)
refresh)}
(or (key= event \j) (key= event :down))
(let [max-idx (max 0 (dec (count (:lines (:diff-hunks model)))))]
{:model (update model :diff-cursor #(min max-idx (inc %)))})
(or (key= event \k) (key= event :up))
{:model (update model :diff-cursor #(max 0 (dec %)))}
;; Space: stage/unstage the hunk containing the current line
(key= event \space)
(let [{:keys [diff-hunks diff-cursor diff-file-item]} model
hunk-idx (line->hunk-index diff-hunks diff-cursor)
patch (when hunk-idx (hunk-to-patch diff-hunks hunk-idx))
staged? (= (:type diff-file-item) :staged)]
(if patch
(do
(if staged?
(git/unapply-patch-cached patch)
(git/apply-patch-cached patch))
(let [path (:path diff-file-item)
new-diff (if staged?
(git/diff-staged-file path)
(git/diff-unstaged path))
new-parsed (when new-diff (parse-diff-hunks new-diff))
new-model (-> model
(assoc :staged (git/staged-files)
:unstaged (git/unstaged-files))
clamp-cursor)
max-line (if new-parsed
(dec (count (:lines new-parsed)))
0)]
(if (and new-parsed (seq (:hunks new-parsed)))
{:model (assoc new-model
:diff new-diff
:diff-hunks new-parsed
:diff-cursor (min diff-cursor max-line))}
{:model (-> new-model
(assoc :diff-focused false :diff-staging false
:diff-hunks nil :diff-file-item nil)
update-diff
(assoc :message (if staged? "Unstaged hunk" "Staged hunk")))})))
{:model model}))
(or (key= event \q) (key= event \c #{:ctrl}))
{:model model :events [(ev/quit)]}
:else
{:model model})
;; Read-only scroll mode
(cond
(key= event :escape)
{:model (assoc model :diff-focused false)}
(or (key= event \j) (key= event :down))
(let [lines (when (:diff model) (str/split-lines (:diff model)))
max-scroll (max 0 (dec (count lines)))]
{:model (update model :diff-scroll #(min max-scroll (inc %)))})
(or (key= event \k) (key= event :up))
{:model (update model :diff-scroll #(max 0 (dec %)))}
(or (key= event \q) (key= event \c #{:ctrl}))
{:model model :events [(ev/quit)]}
:else
{:model model}))
(or (key= event \q) (key= event \c #{:ctrl})) (or (key= event \q) (key= event \c #{:ctrl}))
{:model model :events [(ev/quit)]} {:model model :events [(ev/quit)]}
@@ -660,29 +510,29 @@
;; Panel jump keys (matching lazygit: 2=Files, 3=Branches, 4=Commits, 5=Stash) ;; Panel jump keys (matching lazygit: 2=Files, 3=Branches, 4=Commits, 5=Stash)
(key= event \2) (key= event \2)
{:model (-> model (assoc :panel :files :cursor 0 :diff-focused false) clamp-cursor update-diff)} {:model (-> model (assoc :panel :files :cursor 0) clamp-cursor update-diff)}
(key= event \3) (key= event \3)
{:model (-> model (assoc :panel :branches :cursor 0 :diff-focused false) clamp-cursor update-diff)} {:model (-> model (assoc :panel :branches :cursor 0) clamp-cursor update-diff)}
(key= event \4) (key= event \4)
{:model (-> model (assoc :panel :commits :cursor 0 :diff-focused false) clamp-cursor update-diff)} {:model (-> model (assoc :panel :commits :cursor 0) clamp-cursor update-diff)}
(key= event \5) (key= event \5)
{:model (-> model (assoc :panel :stash :cursor 0 :diff-focused false) clamp-cursor update-diff)} {:model (-> model (assoc :panel :stash :cursor 0) clamp-cursor update-diff)}
;; h/l or Left/Right: move between panels (order: files → branches → commits → stash) ;; h/l or Left/Right: move between panels (order: files → branches → commits → stash)
(or (key= event \h) (key= event :left)) (or (key= event \h) (key= event :left))
{:model (-> model {:model (-> model
(update :panel #(case % :files :stash :branches :files :commits :branches :stash :commits)) (update :panel #(case % :files :stash :branches :files :commits :branches :stash :commits))
(assoc :cursor 0 :diff-focused false) (assoc :cursor 0)
clamp-cursor clamp-cursor
update-diff)} update-diff)}
(or (key= event \l) (key= event :right)) (or (key= event \l) (key= event :right))
{:model (-> model {:model (-> model
(update :panel #(case % :files :branches :branches :commits :commits :stash :stash :files)) (update :panel #(case % :files :branches :branches :commits :commits :stash :stash :files))
(assoc :cursor 0 :diff-focused false) (assoc :cursor 0)
clamp-cursor clamp-cursor
update-diff)} update-diff)}
@@ -748,46 +598,6 @@
(assoc :message (str "Redo: reset to " (:ref entry))))}) (assoc :message (str "Redo: reset to " (:ref entry))))})
{:model (assoc model :message "Nothing to redo")})) {:model (assoc model :message "Nothing to redo")}))
;; Mouse: click tab label to switch tab
(= (:type event) :switch-tab)
(let [{:keys [panel tab]} event
tab-key (case panel
:branches :branches-tab
:commits :commits-tab
nil)]
(if tab-key
{:model (-> model
(assoc :panel panel tab-key tab :cursor 0)
clamp-cursor
update-diff)}
{:model model}))
;; Mouse: click to switch panel
(= (:type event) :switch-panel)
{:model (-> model
(assoc :panel (:panel event) :cursor 0)
clamp-cursor
update-diff)}
;; Mouse: click to select item
(= (:type event) :select-item)
{:model (-> model
(assoc :panel (:panel event) :cursor (:index event))
clamp-cursor
update-diff)}
;; Mouse: click to select diff line in staging view
(= (:type event) :select-diff-line)
{:model (assoc model :diff-cursor (:index event))}
;; Mouse: scroll wheel in panel
(= (:type event) :scroll-panel)
{:model (-> model
(assoc :panel (:panel event))
(update :cursor (if (= (:direction event) :wheel-up) dec inc))
clamp-cursor
update-diff)}
:else :else
(case (:panel model) (case (:panel model)
:files (update-files ctx) :files (update-files ctx)
@@ -816,135 +626,94 @@
(when sync-info [:text {:fg :cyan} sync-info])]))])) (when sync-info [:text {:fg :cyan} sync-info])]))]))
;; Panel 2: Files ;; Panel 2: Files
(defn files-panel [{:keys [staged unstaged cursor panel diff-focused]}] (defn files-panel [{:keys [staged unstaged cursor panel]}]
(let [active? (= panel :files) (let [active? (= panel :files)
focused? (and active? (not diff-focused))
all-files (into (mapv #(assoc % :section :staged) staged) all-files (into (mapv #(assoc % :section :staged) staged)
(mapv #(assoc % :section :unstaged) unstaged)) (mapv #(assoc % :section :unstaged) unstaged))
total (count all-files)] total (count all-files)]
[:box {:border (if focused? :double :single) [:box {:border (if active? :double :single)
:title (str "2 Files (" total ")") :title (str "2 Files (" total ")")
:on-click {:type :switch-panel :panel :files}
:on-scroll {:type :scroll-panel :panel :files}
:padding [0 1] :width :fill :height :fill} :padding [0 1] :width :fill :height :fill}
(if (empty? all-files) (if (empty? all-files)
[:text {:fg :gray} "No changes"] [:text {:fg :gray} "No changes"]
(into [:scroll {:cursor (if active? cursor 0) (into [:scroll {:cursor (if active? cursor 0)}]
:on-scroll {:type :scroll-panel :panel :files}}]
(for [[idx file] (map-indexed vector all-files)] (for [[idx file] (map-indexed vector all-files)]
(let [is-cursor (and active? (= idx cursor)) (let [is-cursor (and active? (= idx cursor))
color (if (= (:section file) :staged) :green :red)] color (if (= (:section file) :staged) :green :red)]
[:row {:gap 1 :on-click {:type :select-item :panel :files :index idx}} [:row {:gap 1}
[:text {:fg color} (file-status-char file)] [:text {:fg color} (file-status-char file)]
[:text {:fg (if is-cursor :cyan color) :bold is-cursor :inverse is-cursor} [:text {:fg (if is-cursor :cyan color) :bold is-cursor :inverse is-cursor}
(truncate (:path file) 20)]]))))])) (truncate (:path file) 20)]]))))]))
;; Panel 3: Branches ;; Panel 3: Branches
(defn- tab-label [text tab-key current-tab] (defn branches-panel [{:keys [branches remote-branches tags branch branches-tab cursor panel]}]
(if (= tab-key current-tab) (str "[" text "]") text))
(defn branches-panel [{:keys [branches remote-branches tags branch branches-tab cursor panel diff-focused]}]
(let [active? (= panel :branches) (let [active? (= panel :branches)
focused? (and active? (not diff-focused)) all-items (case branches-tab :local branches :remotes remote-branches :tags tags branches)
all-items (case branches-tab :local branches :remotes remote-branches :tags tags branches)] tab-str (case branches-tab :local "[L] R T" :remotes "L [R] T" :tags "L R [T]")]
[:box {:border (if focused? :double :single) [:box {:border (if active? :double :single)
:title (str "3 Branches " :title (str "3 Branches " tab-str)
(tab-label "L" :local branches-tab) " "
(tab-label "R" :remotes branches-tab) " "
(tab-label "T" :tags branches-tab))
:on-click {:type :switch-panel :panel :branches}
:on-scroll {:type :scroll-panel :panel :branches}
:padding [0 1] :width :fill :height :fill} :padding [0 1] :width :fill :height :fill}
(if (empty? all-items) (if (empty? all-items)
[:text {:fg :gray} (str "No " (name branches-tab))] [:text {:fg :gray} (str "No " (name branches-tab))]
(into [:scroll {:cursor (if active? cursor 0) (into [:scroll {:cursor (if active? cursor 0)}]
:on-scroll {:type :scroll-panel :panel :branches}}]
(for [[idx item] (map-indexed vector all-items)] (for [[idx item] (map-indexed vector all-items)]
(let [is-cursor (and active? (= idx cursor)) (let [is-cursor (and active? (= idx cursor))
is-current (and (= branches-tab :local) (= item branch)) is-current (and (= branches-tab :local) (= item branch))
fg (cond is-current :green is-cursor :cyan :else :white)] fg (cond is-current :green is-cursor :cyan :else :white)]
[:text {:fg fg :bold (or is-current is-cursor) :inverse is-cursor [:text {:fg fg :bold (or is-current is-cursor) :inverse is-cursor}
:on-click {:type :select-item :panel :branches :index idx}}
(str (if is-current "* " " ") (truncate item 22))]))))])) (str (if is-current "* " " ") (truncate item 22))]))))]))
;; Panel 4: Commits ;; Panel 4: Commits
(defn commits-panel [{:keys [commits reflog commits-tab cursor panel diff-focused]}] (defn commits-panel [{:keys [commits reflog commits-tab cursor panel]}]
(let [active? (= panel :commits) (let [active? (= panel :commits)
focused? (and active? (not diff-focused))
reflog? (= commits-tab :reflog) reflog? (= commits-tab :reflog)
all-items (if reflog? reflog commits)] all-items (if reflog? reflog commits)]
[:box {:border (if focused? :double :single) [:box {:border (if active? :double :single)
:title (str "4 Commits " :title (str "4 Commits " (if reflog? "C [R]" "[C] R"))
(tab-label "C" :commits commits-tab) " "
(tab-label "R" :reflog commits-tab))
:on-click {:type :switch-panel :panel :commits}
:on-scroll {:type :scroll-panel :panel :commits}
:padding [0 1] :width :fill :height :fill} :padding [0 1] :width :fill :height :fill}
(if (empty? all-items) (if (empty? all-items)
[:text {:fg :gray} "No commits"] [:text {:fg :gray} "No commits"]
(into [:scroll {:cursor (if active? cursor 0) (into [:scroll {:cursor (if active? cursor 0)}]
:on-scroll {:type :scroll-panel :panel :commits}}]
(for [[idx {:keys [sha action subject]}] (map-indexed vector all-items)] (for [[idx {:keys [sha action subject]}] (map-indexed vector all-items)]
(let [is-cursor (and active? (= idx cursor))] (let [is-cursor (and active? (= idx cursor))]
[:row {:gap 1 :on-click {:type :select-item :panel :commits :index idx}} [:row {:gap 1}
[:text {:fg :yellow} (or sha "?")] [:text {:fg :yellow} (or sha "?")]
[:text {:fg (if is-cursor :cyan :white) :bold is-cursor :inverse is-cursor} [:text {:fg (if is-cursor :cyan :white) :bold is-cursor :inverse is-cursor}
(truncate (if reflog? (str action ": " subject) (or subject "")) 14)]]))))])) (truncate (if reflog? (str action ": " subject) (or subject "")) 14)]]))))]))
;; Panel 5: Stash ;; Panel 5: Stash
(defn stash-panel [{:keys [stashes cursor panel diff-focused]}] (defn stash-panel [{:keys [stashes cursor panel]}]
(let [active? (= panel :stash) (let [active? (= panel :stash)
focused? (and active? (not diff-focused))
total (count stashes)] total (count stashes)]
[:box {:border (if focused? :double :single) [:box {:border (if active? :double :single)
:title (str "5 Stash (" total ")") :title (str "5 Stash (" total ")")
:on-click {:type :switch-panel :panel :stash}
:on-scroll {:type :scroll-panel :panel :stash}
:padding [0 1] :width :fill :height :fill} :padding [0 1] :width :fill :height :fill}
(if (empty? stashes) (if (empty? stashes)
[:text {:fg :gray} "No stashes"] [:text {:fg :gray} "No stashes"]
(into [:scroll {:cursor (if active? cursor 0) (into [:scroll {:cursor (if active? cursor 0)}]
:on-scroll {:type :scroll-panel :panel :stash}}]
(for [[idx {:keys [index message]}] (map-indexed vector stashes)] (for [[idx {:keys [index message]}] (map-indexed vector stashes)]
(let [is-cursor (and active? (= idx cursor))] (let [is-cursor (and active? (= idx cursor))]
[:row {:gap 1 :on-click {:type :select-item :panel :stash :index idx}} [:row {:gap 1}
[:text {:fg :yellow} (str (or index idx))] [:text {:fg :yellow} (str (or index idx))]
[:text {:fg (if is-cursor :cyan :white) :bold is-cursor :inverse is-cursor} [:text {:fg (if is-cursor :cyan :white) :bold is-cursor :inverse is-cursor}
(truncate (or message "") 20)]]))))])) (truncate (or message "") 20)]]))))]))
(defn diff-line-color [line] ;; Panel 0: Main View (Diff)
(cond (defn main-view-panel [{:keys [diff]}]
(str/starts-with? line "+") :green (let [lines (when diff (str/split-lines diff))]
(str/starts-with? line "-") :red [:box {:border :single :title "0 Main" :padding [0 1] :width :fill :height :fill}
(str/starts-with? line "@@") :cyan
(or (str/starts-with? line "diff")
(str/starts-with? line "commit")) :yellow
:else :white))
;; Main View (Diff)
(defn main-view-panel [{:keys [diff diff-focused diff-scroll
diff-staging diff-hunks diff-cursor
diff-file-item]}]
(let [lines (when diff (str/split-lines diff))
title (if (and diff-staging diff-file-item)
(str "Staging - " (:path diff-file-item))
"Main")]
[:box {:border (if diff-focused :double :single) :title title
:padding [0 1] :width :fill :height :fill}
(if (empty? lines) (if (empty? lines)
[:text {:fg :gray} "Select an item to view diff"] [:text {:fg :gray} "Select an item to view diff"]
(if diff-staging [:col
;; Interactive staging: highlight cursor line, click selects line (for [line lines]
(into [:scroll {:cursor (or diff-cursor 0)}] [:text {:fg (cond
(for [[idx line] (map-indexed vector lines)] (str/starts-with? line "+") :green
[:text {:fg (diff-line-color line) (str/starts-with? line "-") :red
:inverse (= idx diff-cursor) (str/starts-with? line "@@") :cyan
:on-click {:type :select-diff-line :index idx}} (or (str/starts-with? line "diff")
line])) (str/starts-with? line "commit")) :yellow
;; Read-only scroll :else :white)}
(into [:scroll {:cursor (if diff-focused (or diff-scroll 0) 0)}] line])])]))
(for [line lines]
[:text {:fg (diff-line-color line)} line]))))]))
;; Command Log (placeholder) ;; Command Log (placeholder)
(defn command-log-panel [] (defn command-log-panel []
@@ -953,27 +722,22 @@
;; Bottom help bar ;; Bottom help bar
(defn help-bar [model] (defn help-bar [model]
(if (:diff-staging model) (let [panel (:panel model)
[:row {:gap 1} panel-help (case panel
[:text {:fg :gray} "esc:back"] :files "spc:stage a:all c:commit"
[:text {:fg :gray} "j/k:hunks"] :commits "[]:tabs spc:checkout"
[:text {:fg :gray} "spc:stage/unstage hunk"]] :branches "[]:tabs n:new d:del"
(let [panel (:panel model) :stash "spc:apply g:pop d:drop"
panel-help (case panel "")]
:files "spc:stage a:all c:commit enter:staging" (into [:row {:gap 1}]
:commits "[]:tabs spc:checkout enter:view" (remove nil?
:branches "[]:tabs n:new d:del" [[:text {:fg :gray} "q:quit"]
:stash "spc:apply g:pop d:drop enter:view" [:text {:fg :gray} "?:help"]
"")] [:text {:fg :gray} "h/l:panels"]
(into [:row {:gap 1}] [:text {:fg :gray} "j/k:nav"]
(remove nil? (when (seq panel-help)
[[:text {:fg :gray} "q:quit"] [:text {:fg :gray} panel-help])
[:text {:fg :gray} "?:help"] [:text {:fg :gray} "p/P:pull/push"]]))))
[:text {:fg :gray} "h/l:panels"]
[:text {:fg :gray} "j/k:nav"]
(when (seq panel-help)
[:text {:fg :gray} panel-help])
[:text {:fg :gray} "p/P:pull/push"]])))))
(defn stash-menu-view [{:keys [menu-mode]}] (defn stash-menu-view [{:keys [menu-mode]}]
(when (= menu-mode :stash-options) (when (= menu-mode :stash-options)
@@ -1008,8 +772,7 @@
[:text {:fg :cyan :bold true} "Global:"] [:text {:fg :cyan :bold true} "Global:"]
[:text " q - Quit r - Refresh"] [:text " q - Quit r - Refresh"]
[:text " h/l - Prev/Next panel 2-5 - Jump to panel"] [:text " h/l - Prev/Next panel 2-5 - Jump to panel"]
[:text " j/k - Move down/up enter - Focus diff"] [:text " j/k - Move down/up z/Z - Undo/Redo"]
[:text " z/Z - Undo/Redo esc - Return from diff"]
[:text " p - Pull P - Push"] [:text " p - Pull P - Push"]
[:text " ? - Help D - Reset options"] [:text " ? - Help D - Reset options"]
[:text ""] [:text ""]
@@ -1119,16 +882,20 @@
:else :else
background))) background)))
(defn test-view-2 [& args]
[:box "foo"])
;; === Main === ;; === Main ===
(def temp2 get-current-diff)
(defn -main [& _args] (defn -main [& _args]
(if (git/repo-root) (if (git/repo-root)
(do (do
(println "Starting lazygitclj...") (println (str "String with initial model: " (initial-model)))
(tui/run {:init (initial-model) (tui/run {:init (initial-model)
:update update-model :update update-model
:view view :view test-view-2})
:mouse true})
(println "Goodbye!")) (println "Goodbye!"))
(do (do
(println "Error: Not a git repository") (println "Error: Not a git repository")
-18
View File
@@ -436,21 +436,3 @@
"Checkout a tag (detached HEAD)." "Checkout a tag (detached HEAD)."
[name] [name]
(sh "git" "checkout" name)) (sh "git" "checkout" name))
;; === Patch Application ===
(defn apply-patch-cached
"Apply a patch to the index via stdin (stage a hunk)."
[patch-text]
(try
(shell {:in patch-text :out :string :err :string} "git" "apply" "--cached")
true
(catch Exception _ false)))
(defn unapply-patch-cached
"Reverse-apply a patch from the index via stdin (unstage a hunk)."
[patch-text]
(try
(shell {:in patch-text :out :string :err :string} "git" "apply" "--cached" "-R")
true
(catch Exception _ false)))