giterm 1.0.0 → 1.1.1
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 +245 -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: 2012953b93f04a86f90934c30f6329d026b3aceac176ce69f50aa00f962f45a7
|
4
|
+
data.tar.gz: 7f71c894db4123c5d7dc6d28ebb18717e207bc462c8ee6d9d4de53559b840b7a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f73797cd5e08b42a23d50e5e70092b63505e6c412e679dd6cc3532de0887563a359a5693c96377cc0d01516c027ff8a40bd6b29041816c168b65a23a32a5ce04
|
7
|
+
data.tar.gz: 7a1881711325995593be38b5f339d534cc7003642b2e78dd8bafd8d9dd3c3384f078b1ddb83c97293961de52a3f5c679d8b73e339f48961d181219b6849d6129
|
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 = []
|
@@ -1275,6 +1474,7 @@ def github_search_repositories
|
|
1275
1474
|
@mode = :github_search
|
1276
1475
|
@github_search_results = []
|
1277
1476
|
@index = 0
|
1477
|
+
@p_left.ix = 0 # Reset scroll position
|
1278
1478
|
|
1279
1479
|
# Perform the search
|
1280
1480
|
result = github_request("/search/repositories?q=#{CGI.escape(query)}&sort=stars&order=desc&per_page=50")
|
@@ -2049,7 +2249,13 @@ def update_right_pane(fast_update = false)
|
|
2049
2249
|
|
2050
2250
|
case @mode
|
2051
2251
|
when :status
|
2052
|
-
|
2252
|
+
if @is_git_repo
|
2253
|
+
if @status_items.empty?
|
2254
|
+
show_repo_overview
|
2255
|
+
else
|
2256
|
+
show_file_diff(@status_items[@index][:file]) if @index >= 0 && @index < @status_items.length
|
2257
|
+
end
|
2258
|
+
end
|
2053
2259
|
when :log
|
2054
2260
|
show_commit_details(@log_entries[@index]) if @index >= 0 && @index < @log_entries.length
|
2055
2261
|
when :branches
|
@@ -2125,7 +2331,7 @@ def show_help
|
|
2125
2331
|
end
|
2126
2332
|
|
2127
2333
|
def stage_file
|
2128
|
-
return unless @mode == :status && @index < @status_items.length
|
2334
|
+
return unless @is_git_repo && @mode == :status && @index < @status_items.length
|
2129
2335
|
|
2130
2336
|
file = @status_items[@index][:file]
|
2131
2337
|
`git add #{Shellwords.escape(file)}`
|
@@ -2133,7 +2339,7 @@ def stage_file
|
|
2133
2339
|
end
|
2134
2340
|
|
2135
2341
|
def unstage_file
|
2136
|
-
return unless @mode == :status && @index < @status_items.length
|
2342
|
+
return unless @is_git_repo && @mode == :status && @index < @status_items.length
|
2137
2343
|
|
2138
2344
|
file = @status_items[@index][:file]
|
2139
2345
|
`git reset HEAD #{Shellwords.escape(file)}`
|
@@ -2141,6 +2347,11 @@ def unstage_file
|
|
2141
2347
|
end
|
2142
2348
|
|
2143
2349
|
def commit_changes
|
2350
|
+
unless @is_git_repo
|
2351
|
+
@p_bottom.say('Not in a Git repository'.fg(196))
|
2352
|
+
return
|
2353
|
+
end
|
2354
|
+
|
2144
2355
|
@p_bottom.clear
|
2145
2356
|
@p_bottom.say('Commit message: ')
|
2146
2357
|
|
@@ -2358,17 +2569,19 @@ def main_loop
|
|
2358
2569
|
log_debug('Entered main loop')
|
2359
2570
|
loop_count = 0
|
2360
2571
|
loop do
|
2361
|
-
|
2362
|
-
|
2363
|
-
|
2364
|
-
|
2365
|
-
|
2366
|
-
|
2367
|
-
|
2368
|
-
|
2369
|
-
|
2370
|
-
|
2371
|
-
|
2572
|
+
begin
|
2573
|
+
loop_count += 1
|
2574
|
+
log_debug("Main loop iteration #{loop_count}") if loop_count % 100 == 1
|
2575
|
+
render
|
2576
|
+
chr = getkey
|
2577
|
+
handle_key(chr)
|
2578
|
+
rescue => e
|
2579
|
+
log_debug("Error in main loop: #{e.class}: #{e.message}")
|
2580
|
+
log_debug("Backtrace: #{e.backtrace.first(5).join(' | ')}")
|
2581
|
+
@p_bottom.say("Error: #{e.message}") if @p_bottom
|
2582
|
+
sleep 1
|
2583
|
+
# Continue the loop after error
|
2584
|
+
end
|
2372
2585
|
end
|
2373
2586
|
rescue Interrupt
|
2374
2587
|
log_debug('Received interrupt')
|
@@ -2388,12 +2601,12 @@ begin
|
|
2388
2601
|
|
2389
2602
|
log_debug('Checking Git repository...')
|
2390
2603
|
# Check if we're in a Git repository
|
2391
|
-
|
2392
|
-
|
2393
|
-
|
2394
|
-
|
2604
|
+
@is_git_repo = system('git rev-parse --git-dir >/dev/null 2>&1')
|
2605
|
+
if @is_git_repo
|
2606
|
+
log_debug('Git repo check passed')
|
2607
|
+
else
|
2608
|
+
log_debug('Not in Git repo, but continuing...')
|
2395
2609
|
end
|
2396
|
-
log_debug('Git repo check passed')
|
2397
2610
|
|
2398
2611
|
log_debug('Loading configuration...')
|
2399
2612
|
load_config
|
@@ -2403,9 +2616,15 @@ begin
|
|
2403
2616
|
init_windows
|
2404
2617
|
log_debug('Windows initialized')
|
2405
2618
|
|
2406
|
-
|
2407
|
-
|
2408
|
-
|
2619
|
+
if @is_git_repo
|
2620
|
+
log_debug('Getting git status...')
|
2621
|
+
git_status # Start with git status view
|
2622
|
+
log_debug('Git status retrieved')
|
2623
|
+
else
|
2624
|
+
# Start in non-git mode
|
2625
|
+
log_debug('Not in git repo, showing non-git UI...')
|
2626
|
+
show_github_setup_in_nongit
|
2627
|
+
end
|
2409
2628
|
|
2410
2629
|
update_bottom_legends
|
2411
2630
|
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.1
|
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-07 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:
|