Skip to content

Commit e17348e

Browse files
committed
feat: cli 사용성 개선
1 parent 043dde6 commit e17348e

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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

Comments
 (0)