1+ package com.commitchronicle.cli
2+
3+ import java.io.IOException
4+
5+ class InteractiveMenu {
6+ companion object {
7+ private const val ANSI_CLEAR_LINE = " \u001B [2K"
8+ private const val ANSI_CURSOR_UP = " \u001B [1A"
9+ private const val ANSI_CURSOR_DOWN = " \u001B [1B"
10+ private const val ANSI_SAVE_CURSOR = " \u001B [s"
11+ private const val ANSI_RESTORE_CURSOR = " \u001B [u"
12+ private const val ANSI_HIDE_CURSOR = " \u001B [?25l"
13+ private const val ANSI_SHOW_CURSOR = " \u001B [?25h"
14+ private const val ANSI_RESET = " \u001B [0m"
15+ private const val ANSI_BOLD = " \u001B [1m"
16+ private const val ANSI_BLUE = " \u001B [34m"
17+ private const val ANSI_GREEN = " \u001B [32m"
18+
19+ fun <T > showMenu (title : String , options : List <Pair <T , String >>): T {
20+ var selectedIndex = 0
21+
22+ // Enable raw mode for terminal input
23+ enableRawMode()
24+
25+ try {
26+ print (ANSI_HIDE_CURSOR )
27+ println (title)
28+
29+ while (true ) {
30+ // Display menu options
31+ for (i in options.indices) {
32+ val (_, displayText) = options[i]
33+ if (i == selectedIndex) {
34+ println (" ${ANSI_BLUE }${ANSI_BOLD } > $displayText${ANSI_RESET } " )
35+ } else {
36+ println (" $displayText " )
37+ }
38+ }
39+
40+ // Read key input
41+ val key = readKey()
42+
43+ when (key) {
44+ " UP" -> {
45+ selectedIndex = if (selectedIndex > 0 ) selectedIndex - 1 else options.size - 1
46+ }
47+ " DOWN" -> {
48+ selectedIndex = if (selectedIndex < options.size - 1 ) selectedIndex + 1 else 0
49+ }
50+ " ENTER" -> {
51+ // Clear menu
52+ repeat(options.size) {
53+ print (ANSI_CURSOR_UP + ANSI_CLEAR_LINE )
54+ }
55+ print (ANSI_SHOW_CURSOR )
56+ println (" ${ANSI_GREEN } Selected: ${options[selectedIndex].second}${ANSI_RESET } " )
57+ return options[selectedIndex].first
58+ }
59+ " ESC" -> {
60+ print (ANSI_SHOW_CURSOR )
61+ throw InterruptedException (" Menu cancelled" )
62+ }
63+ }
64+
65+ // Move cursor up to redraw menu
66+ repeat(options.size) {
67+ print (ANSI_CURSOR_UP + ANSI_CLEAR_LINE )
68+ }
69+ }
70+ } finally {
71+ print (ANSI_SHOW_CURSOR )
72+ disableRawMode()
73+ }
74+ }
75+
76+ private fun readKey (): String {
77+ val input = System .`in `.read()
78+
79+ return when (input) {
80+ 27 -> { // ESC sequence
81+ val next1 = System .`in `.read()
82+ if (next1 == 91 ) { // [
83+ val next2 = System .`in `.read()
84+ when (next2) {
85+ 65 -> " UP" // A
86+ 66 -> " DOWN" // B
87+ 67 -> " RIGHT" // C
88+ 68 -> " LEFT" // D
89+ else -> " ESC"
90+ }
91+ } else {
92+ " ESC"
93+ }
94+ }
95+ 10 , 13 -> " ENTER" // Enter key
96+ 3 -> " CTRL_C" // Ctrl+C
97+ else -> " OTHER"
98+ }
99+ }
100+
101+ private fun enableRawMode () {
102+ try {
103+ // Try to enable raw mode using stty (Unix/Linux/Mac)
104+ val process = ProcessBuilder (" stty" , " -echo" , " cbreak" )
105+ .inheritIO()
106+ .start()
107+ process.waitFor()
108+ } catch (e: Exception ) {
109+ // Fallback for Windows or if stty is not available
110+ // Raw mode may not work perfectly, but basic functionality should still work
111+ }
112+ }
113+
114+ private fun disableRawMode () {
115+ try {
116+ // Restore normal terminal mode
117+ val process = ProcessBuilder (" stty" , " echo" , " -cbreak" )
118+ .inheritIO()
119+ .start()
120+ process.waitFor()
121+ } catch (e: Exception ) {
122+ // Fallback - just continue
123+ }
124+ }
125+ }
126+ }
0 commit comments