giterm 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +29 -0
- data/giterm +244 -26
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1031719142e9a102c9785ad09e67227777b8fb60ce09ab89e2cf8c8111536e5a
|
4
|
+
data.tar.gz: 8ed0347f1eca624142d53b6516928d3eb95e67eee2ecc31746d5530ca992ba81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce6ecfe8c35cc92d3d5bf5ecb3c44ce880a22c34da13bbed42f0aecaa5ed4072a227e4dcd8dccfb6a571b248c56dc04a520158485f588244701524e5101c0913
|
7
|
+
data.tar.gz: c454493a5c3ae04a5745e6905ae81e9fbbdaab731354f413796842a41ceea83fd389400bb8086b4af033acb2dff07e024c3719e9084c92305911fd7933b0fdfb
|
data/README.md
CHANGED
@@ -51,6 +51,28 @@ A powerful Git and GitHub Terminal User Interface (TUI) client written in Ruby u
|
|
51
51
|
- `:` - Enter git command mode
|
52
52
|
- `!` - Enter shell command mode
|
53
53
|
|
54
|
+
## Screenshots
|
55
|
+
|
56
|
+
### Git Repository Status View
|
57
|
+
Shows the enhanced status view with file changes and detailed diff information in the right pane:
|
58
|
+
|
59
|
+

|
60
|
+
|
61
|
+
### Non-Git Directory Support
|
62
|
+
New in v1.1.0 - GiTerm now runs in non-git directories and offers GitHub integration:
|
63
|
+
|
64
|
+

|
65
|
+
|
66
|
+
### GitHub Repositories Mode
|
67
|
+
Browse all your GitHub repositories with detailed information and README preview:
|
68
|
+
|
69
|
+

|
70
|
+
|
71
|
+
### Help System
|
72
|
+
Comprehensive help showing all available keyboard shortcuts:
|
73
|
+
|
74
|
+

|
75
|
+
|
54
76
|
## Requirements
|
55
77
|
|
56
78
|
- Ruby 2.7 or higher
|
@@ -136,6 +158,13 @@ GiTerm looks for configuration in the following order:
|
|
136
158
|
|
137
159
|
## What's New
|
138
160
|
|
161
|
+
### Version 1.1.0
|
162
|
+
- 🏠 **Non-Git Directory Support** - GiTerm now runs outside git repositories!
|
163
|
+
- 📊 **Enhanced Local Repository Info** - Rich repository overview when working tree is clean
|
164
|
+
- 🔧 **Smart Mode Switching** - Seamless switching between local Git and GitHub modes
|
165
|
+
- 🎯 **Improved User Experience** - Better error handling and helpful guidance
|
166
|
+
- 📸 **Updated Documentation** - New screenshots showing all major features
|
167
|
+
|
139
168
|
### Version 1.0.0
|
140
169
|
- 🎨 Organization color coding in GitHub mode
|
141
170
|
- 📜 Right pane scrolling with Shift+Arrow keys
|
data/giterm
CHANGED
@@ -7,7 +7,7 @@
|
|
7
7
|
# Author: Geir Isene <g@isene.com> (adapted from RTFM)
|
8
8
|
# Github: https://github.com/isene/GiTerm
|
9
9
|
# License: Public domain
|
10
|
-
@version = '1.
|
10
|
+
@version = '1.1.0'
|
11
11
|
|
12
12
|
# SAVE & STORE TERMINAL {{{1
|
13
13
|
ORIG_STTY = `stty -g 2>/dev/null`.chomp rescue ''
|
@@ -421,6 +421,11 @@ def update_bottom_legends
|
|
421
421
|
end
|
422
422
|
|
423
423
|
def update_repo_info
|
424
|
+
unless @is_git_repo
|
425
|
+
@p_top.say('[No Git Repo]'.fg(196) + ' | ' + 'GitHub Mode Available'.fg(154))
|
426
|
+
return
|
427
|
+
end
|
428
|
+
|
424
429
|
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
425
430
|
remote = `git config --get remote.origin.url 2>/dev/null`.strip
|
426
431
|
|
@@ -436,7 +441,7 @@ end
|
|
436
441
|
|
437
442
|
def update_mode_info(mode_title)
|
438
443
|
@current_mode_title = mode_title
|
439
|
-
if @current_branch
|
444
|
+
if @current_branch && @current_repo
|
440
445
|
info = "[#{mode_title}]".fg(156) + " | " + @current_repo.fg(249) + " | " + @current_branch.fg(154)
|
441
446
|
else
|
442
447
|
info = "[#{mode_title}]".fg(156)
|
@@ -447,6 +452,23 @@ end
|
|
447
452
|
# GIT FUNCTIONS {{{1
|
448
453
|
def git_status
|
449
454
|
@mode = :status
|
455
|
+
|
456
|
+
unless @is_git_repo
|
457
|
+
@status_items = []
|
458
|
+
@p_left.clear
|
459
|
+
@p_left.say('Not in a Git repository'.fg(196) + "\n\n" +
|
460
|
+
'Press TAB to switch to GitHub mode'.fg(245))
|
461
|
+
@p_right.clear
|
462
|
+
@p_right.say('To use local Git features:'.fg(156) + "\n\n" +
|
463
|
+
'1. Navigate to a Git repository'.fg(245) + "\n" +
|
464
|
+
'2. Run giterm from within the repository'.fg(245) + "\n\n" +
|
465
|
+
'Or press TAB to use GitHub features'.fg(154))
|
466
|
+
@max_index = 0
|
467
|
+
@min_index = 0
|
468
|
+
@index = 0
|
469
|
+
return
|
470
|
+
end
|
471
|
+
|
450
472
|
@status_items = []
|
451
473
|
@p_left.ix = 0 # Reset scroll position
|
452
474
|
|
@@ -468,6 +490,7 @@ def git_status
|
|
468
490
|
@index = [@index, @min_index].max
|
469
491
|
|
470
492
|
display_status
|
493
|
+
update_right_pane # Show repo overview when working tree is clean
|
471
494
|
end
|
472
495
|
|
473
496
|
def display_status
|
@@ -519,11 +542,101 @@ def display_status
|
|
519
542
|
@p_left.say(content)
|
520
543
|
@p_left.ix = saved_ix
|
521
544
|
|
522
|
-
# Show diff in right pane if file selected
|
523
|
-
|
545
|
+
# Show diff in right pane if file selected, or repo overview if clean
|
546
|
+
if @status_items.empty?
|
547
|
+
show_repo_overview
|
548
|
+
else
|
549
|
+
show_file_diff(@status_items[@index][:file]) if @index >= 0 && @index < @status_items.length
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
def show_repo_overview
|
554
|
+
unless @is_git_repo
|
555
|
+
return
|
556
|
+
end
|
557
|
+
|
558
|
+
content = "Repository Overview\n".b.fg(156)
|
559
|
+
content += ('=' * 30) + "\n\n"
|
560
|
+
|
561
|
+
# Get repository information
|
562
|
+
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
563
|
+
remote = `git config --get remote.origin.url 2>/dev/null`.strip
|
564
|
+
commit_count = `git rev-list --count HEAD 2>/dev/null`.strip
|
565
|
+
last_commit = `git log -1 --pretty=format:'%h %s (%cr by %an)' 2>/dev/null`.strip
|
566
|
+
contributors = `git shortlog -sn 2>/dev/null | head -5`.strip
|
567
|
+
branches_count = `git branch -r 2>/dev/null | wc -l`.strip
|
568
|
+
tags_count = `git tag 2>/dev/null | wc -l`.strip
|
569
|
+
stash_count = `git stash list 2>/dev/null | wc -l`.strip
|
570
|
+
|
571
|
+
# Repository name and remote
|
572
|
+
if !remote.empty?
|
573
|
+
repo_name = File.basename(remote, '.git')
|
574
|
+
content += "Repository: ".fg(249) + repo_name.fg(220) + "\n"
|
575
|
+
content += "Remote: ".fg(249) + remote.fg(245) + "\n"
|
576
|
+
else
|
577
|
+
content += "Repository: ".fg(249) + "Local only (no remote)".fg(245) + "\n"
|
578
|
+
end
|
579
|
+
|
580
|
+
content += "\n"
|
581
|
+
|
582
|
+
# Current branch
|
583
|
+
content += "Current branch: ".fg(249) + branch.fg(154) + "\n"
|
584
|
+
|
585
|
+
# Statistics
|
586
|
+
content += "Total commits: ".fg(249) + commit_count.fg(51) + "\n"
|
587
|
+
content += "Branches: ".fg(249) + branches_count.fg(51) + "\n"
|
588
|
+
content += "Tags: ".fg(249) + tags_count.fg(51) + "\n"
|
589
|
+
|
590
|
+
if stash_count.to_i > 0
|
591
|
+
content += "Stashed changes: ".fg(249) + stash_count.fg(220) + "\n"
|
592
|
+
end
|
593
|
+
|
594
|
+
content += "\n"
|
595
|
+
|
596
|
+
# Last commit
|
597
|
+
if !last_commit.empty?
|
598
|
+
content += "Last commit:\n".fg(156)
|
599
|
+
content += last_commit.fg(245) + "\n\n"
|
600
|
+
end
|
601
|
+
|
602
|
+
# Top contributors
|
603
|
+
if !contributors.empty?
|
604
|
+
content += "Top contributors:\n".fg(156)
|
605
|
+
contributors.lines.each do |line|
|
606
|
+
count, name = line.strip.split("\t", 2)
|
607
|
+
content += " #{count.rjust(4).fg(51)} #{name.fg(249)}\n" if name
|
608
|
+
end
|
609
|
+
content += "\n"
|
610
|
+
end
|
611
|
+
|
612
|
+
# Check for uncommitted changes
|
613
|
+
status = `git status --porcelain 2>/dev/null`
|
614
|
+
if status.empty?
|
615
|
+
content += "✓ Working tree clean".fg(154) + "\n"
|
616
|
+
else
|
617
|
+
modified = status.lines.count { |l| l[0..1].include?('M') }
|
618
|
+
added = status.lines.count { |l| l[0..1].include?('A') }
|
619
|
+
deleted = status.lines.count { |l| l[0..1].include?('D') }
|
620
|
+
untracked = status.lines.count { |l| l[0..1] == '??' }
|
621
|
+
|
622
|
+
content += "Uncommitted changes:\n".fg(220)
|
623
|
+
content += " Modified: #{modified}\n".fg(214) if modified > 0
|
624
|
+
content += " Added: #{added}\n".fg(154) if added > 0
|
625
|
+
content += " Deleted: #{deleted}\n".fg(196) if deleted > 0
|
626
|
+
content += " Untracked: #{untracked}\n".fg(245) if untracked > 0
|
627
|
+
end
|
628
|
+
|
629
|
+
@p_right.clear
|
630
|
+
@p_right.say(content)
|
524
631
|
end
|
525
632
|
|
526
633
|
def show_file_diff(file)
|
634
|
+
unless @is_git_repo
|
635
|
+
@p_right.clear
|
636
|
+
@p_right.say('Not in a Git repository'.fg(196))
|
637
|
+
return
|
638
|
+
end
|
639
|
+
|
527
640
|
diff = `git diff #{Shellwords.escape(file)} 2>/dev/null`
|
528
641
|
diff = `git diff --cached #{Shellwords.escape(file)} 2>/dev/null` if diff.empty?
|
529
642
|
|
@@ -549,6 +662,14 @@ def show_file_diff(file)
|
|
549
662
|
end
|
550
663
|
|
551
664
|
def git_log
|
665
|
+
unless @is_git_repo
|
666
|
+
@p_left.clear
|
667
|
+
@p_left.say('Not in a Git repository'.fg(196))
|
668
|
+
@p_right.clear
|
669
|
+
@p_right.say('Git log unavailable outside repositories'.fg(245))
|
670
|
+
return
|
671
|
+
end
|
672
|
+
|
552
673
|
@mode = :log
|
553
674
|
@log_entries = []
|
554
675
|
@p_left.ix = 0 # Reset scroll position
|
@@ -594,6 +715,14 @@ def show_commit_details(hash)
|
|
594
715
|
end
|
595
716
|
|
596
717
|
def git_branches
|
718
|
+
unless @is_git_repo
|
719
|
+
@p_left.clear
|
720
|
+
@p_left.say('Not in a Git repository'.fg(196))
|
721
|
+
@p_right.clear
|
722
|
+
@p_right.say('Git branches unavailable outside repositories'.fg(245))
|
723
|
+
return
|
724
|
+
end
|
725
|
+
|
597
726
|
@mode = :branches
|
598
727
|
@branches = []
|
599
728
|
@p_left.ix = 0 # Reset scroll position
|
@@ -635,6 +764,11 @@ def display_branches
|
|
635
764
|
end
|
636
765
|
|
637
766
|
def git_pull
|
767
|
+
unless @is_git_repo
|
768
|
+
@p_bottom.say('Not in a Git repository'.fg(196))
|
769
|
+
return
|
770
|
+
end
|
771
|
+
|
638
772
|
@p_bottom.say('Pulling from remote...')
|
639
773
|
result = `git pull 2>&1`
|
640
774
|
@p_bottom.say(result.lines.first.strip)
|
@@ -642,6 +776,11 @@ def git_pull
|
|
642
776
|
end
|
643
777
|
|
644
778
|
def git_push
|
779
|
+
unless @is_git_repo
|
780
|
+
@p_bottom.say('Not in a Git repository'.fg(196))
|
781
|
+
return
|
782
|
+
end
|
783
|
+
|
645
784
|
@p_bottom.say('Pushing to remote...')
|
646
785
|
result = `git push 2>&1`
|
647
786
|
@p_bottom.say(result.lines.first.strip)
|
@@ -772,6 +911,66 @@ def github_request_curl(endpoint)
|
|
772
911
|
end
|
773
912
|
end
|
774
913
|
|
914
|
+
def show_github_setup_in_nongit
|
915
|
+
@mode = :status # Use status mode but show special message
|
916
|
+
@status_items = [] # Empty items list
|
917
|
+
|
918
|
+
content = "GiTerm - GitHub Terminal UI\n".b.fg(156)
|
919
|
+
content += ('=' * 35) + "\n\n"
|
920
|
+
|
921
|
+
content += "Not in a Git repository\n".fg(196)
|
922
|
+
content += "Local Git features unavailable\n\n".fg(245)
|
923
|
+
|
924
|
+
if @github_token.empty?
|
925
|
+
content += "[!] No GitHub token found\n\n".fg(196)
|
926
|
+
content += "To access GitHub features:\n".fg(249)
|
927
|
+
content += "Press ".fg(249) + "'T'".fg(51) + " for guided token setup\n\n".fg(249)
|
928
|
+
content += "Or set GITHUB_TOKEN environment variable\n".fg(245)
|
929
|
+
else
|
930
|
+
content += "GitHub token configured!\n".fg(154)
|
931
|
+
content += "Press TAB to view GitHub repositories\n".fg(249)
|
932
|
+
end
|
933
|
+
|
934
|
+
content += "\n\nKeyboard shortcuts:\n".fg(156)
|
935
|
+
content += "TAB".fg(220) + " - Switch to GitHub mode\n".fg(245)
|
936
|
+
content += "T".fg(220) + " - Set up GitHub token\n".fg(245)
|
937
|
+
content += "q".fg(220) + " - Quit GiTerm\n".fg(245)
|
938
|
+
content += "?".fg(220) + " - Show help\n".fg(245)
|
939
|
+
|
940
|
+
@p_left.clear
|
941
|
+
@p_left.say(content)
|
942
|
+
|
943
|
+
@p_right.clear
|
944
|
+
if @github_token.empty?
|
945
|
+
help_text = "GitHub Setup Guide\n".b.fg(156)
|
946
|
+
help_text += ('=' * 25) + "\n\n"
|
947
|
+
help_text += "1. Press 'T' to start token setup\n".fg(249)
|
948
|
+
help_text += "2. Follow the instructions\n".fg(249)
|
949
|
+
help_text += "3. Create a Personal Access Token\n".fg(249)
|
950
|
+
help_text += " with 'repo' permissions\n".fg(249)
|
951
|
+
help_text += "4. Paste the token when prompted\n\n".fg(249)
|
952
|
+
help_text += "Benefits of GitHub integration:\n".fg(154)
|
953
|
+
help_text += "• Browse all your repositories\n".fg(245)
|
954
|
+
help_text += "• View issues and pull requests\n".fg(245)
|
955
|
+
help_text += "• Search GitHub repositories\n".fg(245)
|
956
|
+
help_text += "• Access private repos\n".fg(245)
|
957
|
+
@p_right.say(help_text)
|
958
|
+
else
|
959
|
+
@p_right.say('Press TAB to switch to GitHub mode'.fg(154))
|
960
|
+
end
|
961
|
+
|
962
|
+
# Set up proper indices for main loop
|
963
|
+
@max_index = 0
|
964
|
+
@min_index = 0
|
965
|
+
@index = 0
|
966
|
+
|
967
|
+
# Update mode info
|
968
|
+
update_mode_info('Not in Git Repository')
|
969
|
+
|
970
|
+
# Show bottom legends
|
971
|
+
update_bottom_legends
|
972
|
+
end
|
973
|
+
|
775
974
|
def github_repos
|
776
975
|
@mode = :github_repos
|
777
976
|
@github_repos = []
|
@@ -2049,7 +2248,13 @@ def update_right_pane(fast_update = false)
|
|
2049
2248
|
|
2050
2249
|
case @mode
|
2051
2250
|
when :status
|
2052
|
-
|
2251
|
+
if @is_git_repo
|
2252
|
+
if @status_items.empty?
|
2253
|
+
show_repo_overview
|
2254
|
+
else
|
2255
|
+
show_file_diff(@status_items[@index][:file]) if @index >= 0 && @index < @status_items.length
|
2256
|
+
end
|
2257
|
+
end
|
2053
2258
|
when :log
|
2054
2259
|
show_commit_details(@log_entries[@index]) if @index >= 0 && @index < @log_entries.length
|
2055
2260
|
when :branches
|
@@ -2125,7 +2330,7 @@ def show_help
|
|
2125
2330
|
end
|
2126
2331
|
|
2127
2332
|
def stage_file
|
2128
|
-
return unless @mode == :status && @index < @status_items.length
|
2333
|
+
return unless @is_git_repo && @mode == :status && @index < @status_items.length
|
2129
2334
|
|
2130
2335
|
file = @status_items[@index][:file]
|
2131
2336
|
`git add #{Shellwords.escape(file)}`
|
@@ -2133,7 +2338,7 @@ def stage_file
|
|
2133
2338
|
end
|
2134
2339
|
|
2135
2340
|
def unstage_file
|
2136
|
-
return unless @mode == :status && @index < @status_items.length
|
2341
|
+
return unless @is_git_repo && @mode == :status && @index < @status_items.length
|
2137
2342
|
|
2138
2343
|
file = @status_items[@index][:file]
|
2139
2344
|
`git reset HEAD #{Shellwords.escape(file)}`
|
@@ -2141,6 +2346,11 @@ def unstage_file
|
|
2141
2346
|
end
|
2142
2347
|
|
2143
2348
|
def commit_changes
|
2349
|
+
unless @is_git_repo
|
2350
|
+
@p_bottom.say('Not in a Git repository'.fg(196))
|
2351
|
+
return
|
2352
|
+
end
|
2353
|
+
|
2144
2354
|
@p_bottom.clear
|
2145
2355
|
@p_bottom.say('Commit message: ')
|
2146
2356
|
|
@@ -2358,17 +2568,19 @@ def main_loop
|
|
2358
2568
|
log_debug('Entered main loop')
|
2359
2569
|
loop_count = 0
|
2360
2570
|
loop do
|
2361
|
-
|
2362
|
-
|
2363
|
-
|
2364
|
-
|
2365
|
-
|
2366
|
-
|
2367
|
-
|
2368
|
-
|
2369
|
-
|
2370
|
-
|
2371
|
-
|
2571
|
+
begin
|
2572
|
+
loop_count += 1
|
2573
|
+
log_debug("Main loop iteration #{loop_count}") if loop_count % 100 == 1
|
2574
|
+
render
|
2575
|
+
chr = getkey
|
2576
|
+
handle_key(chr)
|
2577
|
+
rescue => e
|
2578
|
+
log_debug("Error in main loop: #{e.class}: #{e.message}")
|
2579
|
+
log_debug("Backtrace: #{e.backtrace.first(5).join(' | ')}")
|
2580
|
+
@p_bottom.say("Error: #{e.message}") if @p_bottom
|
2581
|
+
sleep 1
|
2582
|
+
# Continue the loop after error
|
2583
|
+
end
|
2372
2584
|
end
|
2373
2585
|
rescue Interrupt
|
2374
2586
|
log_debug('Received interrupt')
|
@@ -2388,12 +2600,12 @@ begin
|
|
2388
2600
|
|
2389
2601
|
log_debug('Checking Git repository...')
|
2390
2602
|
# Check if we're in a Git repository
|
2391
|
-
|
2392
|
-
|
2393
|
-
|
2394
|
-
|
2603
|
+
@is_git_repo = system('git rev-parse --git-dir >/dev/null 2>&1')
|
2604
|
+
if @is_git_repo
|
2605
|
+
log_debug('Git repo check passed')
|
2606
|
+
else
|
2607
|
+
log_debug('Not in Git repo, but continuing...')
|
2395
2608
|
end
|
2396
|
-
log_debug('Git repo check passed')
|
2397
2609
|
|
2398
2610
|
log_debug('Loading configuration...')
|
2399
2611
|
load_config
|
@@ -2403,9 +2615,15 @@ begin
|
|
2403
2615
|
init_windows
|
2404
2616
|
log_debug('Windows initialized')
|
2405
2617
|
|
2406
|
-
|
2407
|
-
|
2408
|
-
|
2618
|
+
if @is_git_repo
|
2619
|
+
log_debug('Getting git status...')
|
2620
|
+
git_status # Start with git status view
|
2621
|
+
log_debug('Git status retrieved')
|
2622
|
+
else
|
2623
|
+
# Start in non-git mode
|
2624
|
+
log_debug('Not in git repo, showing non-git UI...')
|
2625
|
+
show_github_setup_in_nongit
|
2626
|
+
end
|
2409
2627
|
|
2410
2628
|
update_bottom_legends
|
2411
2629
|
log_debug('Starting main loop...')
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: giterm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Geir Isene
|
8
8
|
autorequire:
|
9
9
|
bindir: "."
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rcurses
|
@@ -54,7 +54,8 @@ dependencies:
|
|
54
54
|
version: '13.0'
|
55
55
|
description: GiTerm is a powerful terminal interface for Git and GitHub, providing
|
56
56
|
an intuitive TUI for repository management, issue tracking, and pull request handling.
|
57
|
-
|
57
|
+
Now works in non-git directories! Features include GitHub integration, enhanced
|
58
|
+
repository overview, smart fetching, and vim-like navigation.
|
58
59
|
email:
|
59
60
|
- g@isene.com
|
60
61
|
executables:
|