Skip to content

Commit

Permalink
improve terminal output
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronFeledy committed Jul 5, 2024
1 parent 063391c commit 468587a
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 138 deletions.
89 changes: 28 additions & 61 deletions src/main/kotlin/dev/_4lando/intellij/LandoExec.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package dev._4lando.intellij

import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.execution.process.KillableColoredProcessHandler
import com.intellij.openapi.project.Project
import dev._4lando.intellij.services.LandoProjectService
import dev._4lando.intellij.ui.console.JeditermConsoleView
import java.io.File
import java.io.OutputStream
import com.pty4j.PtyProcessBuilder
import java.util.*

/**
* Class for executing Lando commands.
Expand All @@ -17,7 +17,10 @@ class LandoExec(private val command: String) {

private var directory: String = ""

private var format: String = ""
/**
* The --format parameter for the Lando command.
*/
private var format: Format? = null

private var attachToConsole: Boolean = true

Expand All @@ -39,7 +42,7 @@ class LandoExec(private val command: String) {
* @return The LandoExec instance.
*/
fun format(format: Format): LandoExec {
this.format = format.format
this.format = format
return this
}

Expand All @@ -57,90 +60,54 @@ class LandoExec(private val command: String) {
* @return The ProcessHandler for the process running the Lando command.
*/
fun run(): ProcessHandler {
// Create a new process builder with the Lando binary and the command
val processBuilder = ProcessBuilder(landoBin, command)
var cmd = arrayOf(landoBin, command)

// Get the LandoAppService instance
val appService = LandoAppService.getInstance()
val processBuilder = PtyProcessBuilder()
processBuilder.setRedirectErrorStream(true)
processBuilder.setConsole(true)

// Default to the project root as working directory if none is set
if (directory.isEmpty() && project != null) {
directory = LandoProjectService.getInstance(project!!).appRoot?.path!!
} else if (directory.isEmpty()) {
directory = System.getProperty("user.dir")
}
// Set the directory for the process builder
processBuilder.directory(File(directory))
processBuilder.setDirectory(directory)

// If a format is set, add it to the command
if (format.isNotEmpty()) {
processBuilder.command().add(format)
if (format != null) {
cmd += format.toString()
}

// Start the process
val process = processBuilder.start()
val process = processBuilder.setCommand(cmd).start()

// Attach process to console view to display output
var landoConsoleView: JeditermConsoleView? = null
if (attachToConsole) {
if (attachToConsole && project != null) {
// Create a new console view with the process
landoConsoleView = JeditermConsoleView(appService.thisProject.project, process)
landoConsoleView = JeditermConsoleView(project!!)
landoConsoleView.connectToProcess(process)
}

// Create a new process handler
val processHandler = object : ProcessHandler() {
// Method to destroy the process
override fun destroyProcessImpl() {
process.destroy()
}

// Method to detach the process
override fun detachProcessImpl() {
destroyProcessImpl()
}

// Method to check if detach is default
override fun detachIsDefault(): Boolean = false

// Method to get the process input
override fun getProcessInput(): OutputStream? = null

// Method to start the notification
override fun startNotify() {
super.startNotify()
if (landoConsoleView == null) {
return
}
// Start a new thread to read the process output and print it to the console view
Thread {
val reader = process.inputStream.bufferedReader()
reader.useLines { lines ->
lines.forEach { line ->
// Parse the line to remove any ANSI escape codes
val formattedLine = AnsiEscapeCodeParser.parse(line)
// Print the formatted line to the console view
landoConsoleView.print(formattedLine, ConsoleViewContentType.NORMAL_OUTPUT)
}
}
}.start()
}
val scanner = Scanner(process.inputStream).useDelimiter("\\n")
while (scanner.hasNext()) {
val line = scanner.next()
landoConsoleView?.output((line + "\n").toByteArray())
}

// Attach the process handler to the console view
landoConsoleView?.attachToProcess(processHandler)
// Start the process handler notification
processHandler.startNotify()

// Return the process handler
return processHandler
// Create a new process handler from the process
return KillableColoredProcessHandler(process, cmd.joinToString(" "), Charsets.UTF_8)
}

/**
* Companion object for LandoExec class.
*/
companion object {
const val PROCESS_TIMEOUT = 30000L

/**
* Enum for Lando command formats.
* Enum for the Lando --format parameter values.
*/
enum class Format(val format: String) {
JSON("--format json"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,37 @@ package dev._4lando.intellij.ui.console

import com.intellij.execution.filters.Filter
import com.intellij.execution.filters.HyperlinkInfo
import com.intellij.execution.process.AnsiEscapeDecoder
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.execution.ui.ConsoleView
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.icons.AllIcons
import com.intellij.idea.ActionsBundle
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ToggleAction
import com.intellij.openapi.project.Project
import com.intellij.terminal.JBTerminalSystemSettingsProviderBase
import com.intellij.terminal.JBTerminalWidget
import com.intellij.openapi.util.Key
import com.jediterm.terminal.*
import com.jediterm.terminal.emulator.JediEmulator
import com.jediterm.terminal.model.JediTerminal
import com.pty4j.PtyProcess
import org.apache.commons.io.input.buffer.CircularByteBuffer
import org.jetbrains.plugins.terminal.JBTerminalSystemSettingsProvider
import org.jetbrains.plugins.terminal.ShellTerminalWidget
import java.io.InputStream
import java.io.InputStreamReader
import java.io.Reader
import java.nio.charset.Charset
import javax.swing.JComponent
import kotlin.math.min
import org.apache.commons.io.input.buffer.CircularByteBuffer

private const val BUFFER_SIZE = 100000

class JeditermConsoleView(project: Project, process: Process) : ConsoleView {
class JeditermConsoleView(project: Project) : ConsoleView, AnsiEscapeDecoder.ColoredTextAcceptor {

private val ansiEscapeDecoder = AnsiEscapeDecoder()

private val widget: JBTerminalWidget
val termWidget: ShellTerminalWidget

private val ttyConnector = LandoTtyConnector(process, this)
private var ttyConnector: LandoTtyConnector? = null

private var emulator: CustomJeditermEmulator? = null
private var emulator: JediEmulator? = null

private val bytesBuffer = CircularByteBuffer(BUFFER_SIZE)
private val lock = Object()
Expand All @@ -44,7 +43,6 @@ class JeditermConsoleView(project: Project, process: Process) : ConsoleView {
@Volatile
var paused: Boolean = false


private val bytesStream = object : InputStream() {
override fun read(): Int {
synchronized(lock) {
Expand Down Expand Up @@ -73,42 +71,44 @@ class JeditermConsoleView(project: Project, process: Process) : ConsoleView {
}
}


init {
widget = object : JBTerminalWidget(project, JBTerminalSystemSettingsProviderBase(), this) {
termWidget = object : ShellTerminalWidget(project, JBTerminalSystemSettingsProvider(), this) {
override fun createTerminalStarter(terminal: JediTerminal, connector: TtyConnector): TerminalStarter =
object : TerminalStarter(
terminal, connector,
TtyBasedArrayDataStream(connector) { typeAheadManager.onTerminalStateChanged() },
typeAheadManager, executorServiceManager
) {
override fun createEmulator(dataStream: TerminalDataStream, terminal: Terminal): JediEmulator =
CustomJeditermEmulator(dataStream, terminal).apply { emulator = this }
JediEmulator(dataStream, terminal).apply { emulator = this }
}
}
widget.start(ttyConnector)
//Disposer.register(this, connection)
}

fun connectToProcess(process: PtyProcess) {
ttyConnector = LandoTtyConnector(process, this)
termWidget.start(ttyConnector)
}

override fun dispose() {
ttyConnector?.close()
}

override fun getComponent(): JComponent = widget
override fun getPreferredFocusableComponent(): JComponent = widget
override fun getComponent(): JComponent = termWidget
override fun getPreferredFocusableComponent(): JComponent = termWidget

override fun print(text: String, contentType: ConsoleViewContentType) {
throw NotImplementedError("Not supported")
termWidget.terminal.writeCharacters(text)
}

override fun clear() {
widget.terminalTextBuffer.historyBuffer.clearAll()
widget.terminal.clearScreen()
widget.terminal.cursorPosition(0, 1)
termWidget.terminalTextBuffer.historyBuffer.clearAll()
termWidget.terminal.clearScreen()
termWidget.terminal.cursorPosition(0, 1)
}

override fun scrollTo(offset: Int) {
widget.terminal.setScrollingRegion(offset, Int.MAX_VALUE)
termWidget.terminal.setScrollingRegion(offset, Int.MAX_VALUE)
}

override fun attachToProcess(processHandler: ProcessHandler) {
Expand Down Expand Up @@ -140,7 +140,7 @@ class JeditermConsoleView(project: Project, process: Process) : ConsoleView {
}

override fun getContentSize(): Int =
with(widget.terminalTextBuffer) { screenLinesCount + widget.terminalTextBuffer.screenLinesCount }
with(termWidget.terminalTextBuffer) { screenLinesCount + termWidget.terminalTextBuffer.screenLinesCount }

override fun canPause(): Boolean = true

Expand All @@ -156,22 +156,13 @@ class JeditermConsoleView(project: Project, process: Process) : ConsoleView {
val length = min(dataChunk.size, bytesBuffer.space)
if (length > 0) {
bytesBuffer.add(dataChunk, 0, length)
addData(dataChunk.decodeToString(0, length), ProcessOutputTypes.STDOUT)
lock.notify()
}
}
}
}

fun reconnect(charset: Charset, localEcho: Boolean) {
widget.terminal.setAutoNewLine(true) //todo LF mode is not supported due JediTerm limitations
ttyConnector.charset = charset
ttyConnector.localEcho = localEcho
synchronized(lock) {
bytesBuffer.clear()
bufferReader = InputStreamReader(bytesStream, charset)
}
}

fun readChars(buf: CharArray, offset: Int, length: Int): Int {
synchronized(lock) {
while (true) {
Expand All @@ -187,27 +178,11 @@ class JeditermConsoleView(project: Project, process: Process) : ConsoleView {
}
}

val scrollToTheEndToolbarAction = object : ToggleAction(
ActionsBundle.messagePointer("action.EditorConsoleScrollToTheEnd.text"),
ActionsBundle.messagePointer("action.EditorConsoleScrollToTheEnd.text"),
AllIcons.RunConfigurations.Scroll_down
) {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT

override fun isSelected(e: AnActionEvent): Boolean {
return widget.terminalPanel.verticalScrollModel.value == 0
}

override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = widget.isShowing
}

override fun setSelected(e: AnActionEvent, state: Boolean) {
if (state) {
widget.terminalPanel.verticalScrollModel.value = 0
}
}
private fun addData(message: String, outputType: Key<*>) {
ansiEscapeDecoder.escapeText(message, outputType, this)
}


override fun coloredTextAvailable(text: String, attributes: Key<*>) {
print(text, ConsoleViewContentType.getConsoleViewType(attributes))
}
}
Loading

0 comments on commit 468587a

Please sign in to comment.