package cc.unitmesh.devins.ui.compose.terminal import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.awt.SwingPanel import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import com.jediterm.terminal.TtyConnector import com.jediterm.terminal.ui.JediTermWidget import com.jediterm.terminal.ui.settings.DefaultSettingsProvider import java.awt.Dimension import java.awt.Font import java.io.IOException import java.nio.charset.Charset import java.util.function.Supplier import javax.swing.JScrollBar /** * Custom JediTerm settings provider that integrates with Compose Material Theme. * Inspired by IDEA's JBTerminalSystemSettingsProvider implementation. * Uses FiraCode font for better code readability. */ class ComposeTerminalSettingsProvider( private val backgroundColor: Color, private val foregroundColor: Color, private val selectionColor: Color, private val cursorColor: Color = Color(0xFF64B5F6), // Material blue by default private val terminalFont: Font? = null ) : DefaultSettingsProvider() { // Convert Compose Color to JediTerm Color supplier private fun Color.toJediColorSupplier(): Supplier { val argb = this.toArgb() val jediColor = com.jediterm.core.Color( (argb shr 16) and 0xFF, // red (argb shr 8) and 0xFF, // green argb and 0xFF // blue ) return Supplier { jediColor } } override fun getDefaultStyle(): com.jediterm.terminal.TextStyle { return com.jediterm.terminal.TextStyle( com.jediterm.terminal.TerminalColor(foregroundColor.toJediColorSupplier()), com.jediterm.terminal.TerminalColor(backgroundColor.toJediColorSupplier()) ) } override fun getFoundPatternColor(): com.jediterm.terminal.TextStyle { return com.jediterm.terminal.TextStyle( com.jediterm.terminal.TerminalColor(foregroundColor.toJediColorSupplier()), com.jediterm.terminal.TerminalColor(selectionColor.toJediColorSupplier()) ) } override fun getSelectionColor(): com.jediterm.terminal.TextStyle { return com.jediterm.terminal.TextStyle( com.jediterm.terminal.TerminalColor(foregroundColor.toJediColorSupplier()), com.jediterm.terminal.TerminalColor(selectionColor.toJediColorSupplier()) ) } // Use FiraCode font with smaller size (12pt instead of default 14pt) override fun getTerminalFont(): Font { return terminalFont ?: Font("Monospaced", Font.PLAIN, 12) } // Use the same font settings as IDEA override fun useAntialiasing(): Boolean = true override fun maxRefreshRate(): Int = 50 // Enable modern terminal features override fun audibleBell(): Boolean = false override fun copyOnSelect(): Boolean = true override fun pasteOnMiddleMouseClick(): Boolean = true } /** * TtyConnector implementation that wraps a Process (typically from Pty4J). * This bridges Pty4J processes to JediTerm's terminal widget. */ class ProcessTtyConnector( private val process: Process, private val charset: Charset = Charsets.UTF_8, /** * Optional hook for mirroring terminal output (PTY stream) to another consumer. * * Important: This must be non-blocking. It is called on JediTerm's reader thread. */ private val onReadChunk: ((String) -> Unit)? = null ) : TtyConnector { @Volatile private var closed = false override fun read(buf: CharArray, offset: Int, length: Int): Int { if (closed) return -1 return try { val bytes = ByteArray(length) val bytesRead = process.inputStream.read(bytes, 0, length) if (bytesRead <= 0) return bytesRead val chunk = String(bytes, 0, bytesRead, charset) onReadChunk?.invoke(chunk) val chars = chunk.toCharArray() val copied = chars.size.coerceAtMost(length) System.arraycopy(chars, 0, buf, offset, copied) copied } catch (e: IOException) { if (!closed) throw e -1 } } override fun write(bytes: ByteArray) { if (closed) return try { process.outputStream.write(bytes) process.outputStream.flush() } catch (e: IOException) { if (!closed) throw e } } override fun write(string: String) { write(string.toByteArray(charset)) } override fun isConnected(): Boolean { return !closed && process.isAlive } override fun waitFor(): Int { return try { process.waitFor() } catch (e: InterruptedException) { Thread.currentThread().interrupt() -1 } } override fun ready(): Boolean { return !closed && process.isAlive } override fun close() { closed = true try { process.outputStream.close() process.inputStream.close() process.errorStream?.close() process.destroy() } catch (e: IOException) { // Ignore } } override fun getName(): String { return "AutoDev Pty Process" } } /** * Compose wrapper for JediTerm terminal widget. * Bridges Swing-based JediTerm to Compose UI. */ /** * Holder for scrollbar colors provider to work around JediTermWidget constructor timing. * createScrollBar() is called during parent constructor before field initialization, * so we need a mutable holder that can be set after construction. */ class ScrollbarColorsHolder { var provider: (() -> TerminalScrollbarColors)? = null } /** * Custom JediTermWidget that overrides scrollbar creation like IDEA's JBTerminalWidget. * Following IDEA's pattern: createScrollBar() is called during parent constructor, * so we use a holder approach to access colors after initialization. * Uses ultra-thin, semi-transparent scrollbar for modern look. * Supports dynamic theme changes via color provider. */ class AutoDevTerminalWidget( settingsProvider: ComposeTerminalSettingsProvider, private val colorsHolder: ScrollbarColorsHolder ) : JediTermWidget(settingsProvider) { override fun createScrollBar(): JScrollBar { // Like JBTerminalWidget, create scrollbar that adapts to terminal panel background // Use anonymous class like IDEA does to access terminalPanel after initialization // Use holder to get colors - may be null during parent constructor, but will be set after val bar = object : ModernTerminalScrollBar( VERTICAL, { colorsHolder.provider?.invoke() } ) { override fun getBackground(): java.awt.Color { // Return terminal panel background like JBScrollBar does return terminalPanel?.background ?: super.getBackground() } } bar.isOpaque = false // Make scrollbar non-opaque for transparency bar.unitIncrement = 10 bar.blockIncrement = 48 return bar } } /** * Load FiraCode font for terminal use. * Falls back to Monospaced if FiraCode is not available. */ private fun loadTerminalFont(): Font { return try { // Try to load FiraCode from resources val fontStream = object {}.javaClass.getResourceAsStream("/fonts/FiraCode-Regular.ttf") if (fontStream != null) { val baseFont = Font.createFont(Font.TRUETYPE_FONT, fontStream) // Use smaller font size (12pt) for better terminal density baseFont.deriveFont(12f) } else { // Fallback to system monospaced font Font("Monospaced", Font.PLAIN, 12) } } catch (e: Exception) { println("Failed to load FiraCode font for terminal: ${e.message}, using Monospaced") Font("Monospaced", Font.PLAIN, 12) } } /** * Modern terminal widget component with Material Theme integration. * Inspired by IntelliJ IDEA's JBTerminalWidget implementation. * * Features: * - Material3 color scheme integration * - FiraCode font with smaller size (12pt) * - Ultra-thin, auto-hide scrollbar * - Antialiasing and modern rendering * - Copy on select and paste on middle click */ @Composable fun TerminalWidget( ttyConnector: TtyConnector?, modifier: Modifier = Modifier, onTerminalReady: ((JediTermWidget) -> Unit)? = null ) { var terminalWidget by remember { mutableStateOf(null) } // Get Material Theme colors val backgroundColor = MaterialTheme.colorScheme.surface val foregroundColor = MaterialTheme.colorScheme.onSurface val selectionColor = MaterialTheme.colorScheme.primaryContainer val cursorColor = MaterialTheme.colorScheme.primary val scrollbarThumbColor = MaterialTheme.colorScheme.onSurfaceVariant val terminalFont = remember { loadTerminalFont() } DisposableEffect(Unit) { onDispose { terminalWidget?.close() } } fun Color.toAwtColor(): java.awt.Color = java.awt.Color(this.toArgb(), true) // Use mutable state to track current theme colors for dynamic updates var currentBackgroundColor by remember { mutableStateOf(backgroundColor) } var currentScrollbarThumbColor by remember { mutableStateOf(scrollbarThumbColor) } var currentCursorColor by remember { mutableStateOf(cursorColor) } // Update colors when theme changes LaunchedEffect(backgroundColor, scrollbarThumbColor, cursorColor) { currentBackgroundColor = backgroundColor currentScrollbarThumbColor = scrollbarThumbColor currentCursorColor = cursorColor } // Create color provider lambda that captures current colors val scrollbarColorsProvider: () -> TerminalScrollbarColors = { TerminalScrollbarColors( track = currentBackgroundColor.copy(alpha = 0f).toAwtColor(), thumb = currentScrollbarThumbColor.toAwtColor(), thumbHover = currentScrollbarThumbColor.toAwtColor(), thumbPressed = currentCursorColor.toAwtColor() ) } // Create holder for colors provider - needed because createScrollBar() is called during parent constructor val colorsHolder = remember { ScrollbarColorsHolder() } colorsHolder.provider = scrollbarColorsProvider SwingPanel( modifier = modifier, background = backgroundColor, factory = { val settingsProvider = ComposeTerminalSettingsProvider( backgroundColor = backgroundColor, foregroundColor = foregroundColor, selectionColor = selectionColor, cursorColor = cursorColor, terminalFont = terminalFont ) // Create custom terminal widget with colors holder for dynamic theme support val widget = AutoDevTerminalWidget(settingsProvider, colorsHolder) // Set size constraints widget.preferredSize = Dimension(800, 400) widget.minimumSize = Dimension(400, 200) terminalWidget = widget // Connect to TTY if provided ttyConnector?.let { connector -> widget.ttyConnector = connector widget.start() } onTerminalReady?.invoke(widget) widget }, update = { widget -> // Update terminal if ttyConnector changes if (ttyConnector != null && widget.ttyConnector != ttyConnector) { widget.ttyConnector = ttyConnector widget.start() } } ) } /** * Simple terminal output display for showing command results. * This is a read-only terminal view for displaying shell command output with ANSI support. * * Note: This component is designed to be used inside LazyColumn, * so it does NOT include scroll modifiers to avoid nested scrolling conflicts. */ @Composable fun TerminalOutputDisplay( output: String, modifier: Modifier = Modifier ) { // Use ANSI terminal renderer for proper color and formatting support Box( modifier = modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) .padding(8.dp) ) { AnsiTerminalRenderer( ansiText = output ) } } /** * JVM implementation of platform-specific terminal display */ @Composable actual fun PlatformTerminalDisplay( output: String, modifier: Modifier ) { TerminalOutputDisplay(output, modifier) }