feat!: TTY support for zig processes

This commit is contained in:
FalsePattern 2025-03-27 20:49:50 +01:00
parent da38433eb3
commit 0a5a765eaf
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
16 changed files with 103 additions and 143 deletions

View file

@ -17,6 +17,16 @@ Changelog structure reference:
## [Unreleased] ## [Unreleased]
### Added
- Project, Debugging
- TTY support for zig processes
### Removed
- Project
- "Emulate terminal" and "colored output" config options have been removed from zig run/test/build tasks, as they are no longer required for ZigBrains to work.
### Fixed ### Fixed
- Zig - Zig

View file

@ -57,8 +57,8 @@ class ZigClionDebuggerDriverConfigurationProvider: ZigDebuggerDriverConfiguratio
} }
return when(toolchain.debuggerKind) { return when(toolchain.debuggerKind) {
CPPDebugger.Kind.BUNDLED_GDB, CPPDebugger.Kind.BUNDLED_GDB,
CPPDebugger.Kind.CUSTOM_GDB -> CLionGDBDriverConfiguration(project, toolchain) CPPDebugger.Kind.CUSTOM_GDB -> CLionGDBDriverConfiguration(project, toolchain, isEmulateTerminal = emulateTerminal)
CPPDebugger.Kind.BUNDLED_LLDB -> CLionLLDBDriverConfiguration(project, toolchain) CPPDebugger.Kind.BUNDLED_LLDB -> CLionLLDBDriverConfiguration(project, toolchain, isEmulateTerminal = emulateTerminal)
} }
} }
} }

View file

@ -22,9 +22,10 @@
package com.falsepattern.zigbrains.debugger package com.falsepattern.zigbrains.debugger
import com.intellij.execution.filters.Filter
import com.intellij.execution.filters.TextConsoleBuilder import com.intellij.execution.filters.TextConsoleBuilder
import com.intellij.xdebugger.XDebugSession import com.intellij.xdebugger.XDebugSession
import com.jetbrains.cidr.execution.RunParameters import com.jetbrains.cidr.execution.RunParameters
import com.jetbrains.cidr.execution.debugger.CidrLocalDebugProcess import com.jetbrains.cidr.execution.debugger.CidrLocalDebugProcess
class ZigLocalDebugProcess(parameters: RunParameters, session: XDebugSession, consoleBuilder: TextConsoleBuilder) : CidrLocalDebugProcess(parameters, session, consoleBuilder) class ZigLocalDebugProcess(parameters: RunParameters, session: XDebugSession, consoleBuilder: TextConsoleBuilder) : CidrLocalDebugProcess(parameters, session, consoleBuilder, { Filter.EMPTY_ARRAY }, true)

View file

@ -23,6 +23,7 @@
package com.falsepattern.zigbrains.debugger.runner.base package com.falsepattern.zigbrains.debugger.runner.base
import com.falsepattern.zigbrains.project.run.ZigProcessHandler import com.falsepattern.zigbrains.project.run.ZigProcessHandler
import com.falsepattern.zigbrains.shared.cli.startIPCAwareProcess
import com.falsepattern.zigbrains.shared.ipc.IPCUtil import com.falsepattern.zigbrains.shared.ipc.IPCUtil
import com.falsepattern.zigbrains.shared.ipc.ipc import com.falsepattern.zigbrains.shared.ipc.ipc
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
@ -48,12 +49,7 @@ class PreLaunchProcessListener(val console: ConsoleView) : ProcessListener {
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
suspend fun executeCommandLineWithHook(project: Project, commandLine: GeneralCommandLine): Boolean { suspend fun executeCommandLineWithHook(project: Project, commandLine: GeneralCommandLine): Boolean {
return withProgressText(commandLine.commandLineString) { return withProgressText(commandLine.commandLineString) {
val ipc = IPCUtil.wrapWithIPC(commandLine) val processHandler = commandLine.startIPCAwareProcess(project)
val cli = ipc?.cli ?: commandLine
val processHandler = ZigProcessHandler(cli)
if (ipc != null) {
project.ipc?.launchWatcher(ipc, processHandler.process)
}
this@PreLaunchProcessListener.processHandler = processHandler this@PreLaunchProcessListener.processHandler = processHandler
hook(processHandler) hook(processHandler)
processHandler.startNotify() processHandler.startNotify()
@ -74,10 +70,6 @@ class PreLaunchProcessListener(val console: ConsoleView) : ProcessListener {
override fun processTerminated(event: ProcessEvent) { override fun processTerminated(event: ProcessEvent) {
if (event.exitCode != 0) { if (event.exitCode != 0) {
console.print(
"Process finished with exit code " + event.exitCode,
ConsoleViewContentType.NORMAL_OUTPUT
)
isBuildFailed = true isBuildFailed = true
} else { } else {
isBuildFailed = false isBuildFailed = false

View file

@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PtyCommandLine
import com.jetbrains.cidr.execution.Installer import com.jetbrains.cidr.execution.Installer
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.future.asCompletableFuture
@ -39,7 +40,7 @@ class ZigDebugEmitBinaryInstaller<ProfileState: ZigProfileState<*>>(
): Installer { ): Installer {
override fun install(): GeneralCommandLine { override fun install(): GeneralCommandLine {
val cfg = profileState.configuration val cfg = profileState.configuration
val cli = GeneralCommandLine().withExePath(executableFile.absolutePath) val cli = PtyCommandLine().withConsoleMode(false).withExePath(executableFile.absolutePath)
cfg.workingDirectory.path?.let { x -> cli.withWorkingDirectory(x) } cfg.workingDirectory.path?.let { x -> cli.withWorkingDirectory(x) }
cli.addParameters(exeArgs) cli.addParameters(exeArgs)
cli.withCharset(Charsets.UTF_8) cli.withCharset(Charsets.UTF_8)

View file

@ -58,7 +58,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
val project = environment.project val project = environment.project
val driverProviders = ZigDebuggerDriverConfigurationProviderBase.EXTENSION_POINT_NAME.extensionList val driverProviders = ZigDebuggerDriverConfigurationProviderBase.EXTENSION_POINT_NAME.extensionList
for (provider in driverProviders) { for (provider in driverProviders) {
val driver = provider.getDebuggerConfiguration(project, isElevated = false, emulateTerminal = false, DebuggerDriverConfiguration::class.java) ?: continue val driver = provider.getDebuggerConfiguration(project, isElevated = false, emulateTerminal = true, DebuggerDriverConfiguration::class.java) ?: continue
return executeWithDriver(state, toolchain, environment, driver) ?: continue return executeWithDriver(state, toolchain, environment, driver) ?: continue
} }
return null return null
@ -98,7 +98,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
debuggerManager.startSession(environment, object: XDebugProcessStarter() { debuggerManager.startSession(environment, object: XDebugProcessStarter() {
override fun start(session: XDebugSession): XDebugProcess { override fun start(session: XDebugSession): XDebugProcess {
val project = session.project val project = session.project
val textConsoleBuilder = SharedConsoleBuilder(console) val textConsoleBuilder = state.consoleBuilder
val debugProcess = ZigLocalDebugProcess(runParameters, session, textConsoleBuilder) val debugProcess = ZigLocalDebugProcess(runParameters, session, textConsoleBuilder)
ProcessTerminatedListener.attach(debugProcess.processHandler, project) ProcessTerminatedListener.attach(debugProcess.processHandler, project)
debugProcess.start() debugProcess.start()

View file

@ -0,0 +1,38 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.project.execution
import com.intellij.execution.filters.TextConsoleBuilderImpl
import com.intellij.execution.ui.ConsoleView
import com.intellij.openapi.project.Project
import com.intellij.terminal.TerminalExecutionConsole
import java.nio.file.Path
class ZigConsoleBuilder(private val project: Project, private val emulateTerminal: Boolean = false): TextConsoleBuilderImpl(project) {
override fun createConsole(): ConsoleView {
return if (emulateTerminal)
TerminalExecutionConsole(project, null)
else
super.createConsole()
}
}

View file

@ -274,12 +274,6 @@ open class CheckboxConfigurable(
} }
} }
class ColoredConfigurable(serializedName: String): CheckboxConfigurable(serializedName, ZigBrainsBundle.message("exec.option.label.colored-terminal"), true) {
override fun clone(): ColoredConfigurable {
return super.clone() as ColoredConfigurable
}
}
class OptimizationConfigurable( class OptimizationConfigurable(
@Transient private val serializedName: String, @Transient private val serializedName: String,
var level: OptimizationLevel = OptimizationLevel.Debug, var level: OptimizationLevel = OptimizationLevel.Debug,

View file

@ -43,8 +43,6 @@ import org.jetbrains.annotations.Nls
abstract class ZigExecConfig<T: ZigExecConfig<T>>(project: Project, factory: ConfigurationFactory, @Nls name: String): LocatableConfigurationBase<ZigProfileState<T>>(project, factory, name) { abstract class ZigExecConfig<T: ZigExecConfig<T>>(project: Project, factory: ConfigurationFactory, @Nls name: String): LocatableConfigurationBase<ZigProfileState<T>>(project, factory, name) {
var workingDirectory = WorkDirectoryConfigurable("workingDirectory").apply { path = project.guessProjectDir()?.toNioPathOrNull() } var workingDirectory = WorkDirectoryConfigurable("workingDirectory").apply { path = project.guessProjectDir()?.toNioPathOrNull() }
private set private set
var pty = CheckboxConfigurable("pty", ZigBrainsBundle.message("exec.option.label.emulate-terminal"), false)
private set
abstract val suggestedName: @ActionText String abstract val suggestedName: @ActionText String
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
@ -73,17 +71,12 @@ abstract class ZigExecConfig<T: ZigExecConfig<T>>(project: Project, factory: Con
return commandLine return commandLine
} }
fun emulateTerminal(): Boolean {
return pty.value
}
override fun clone(): T { override fun clone(): T {
val myClone = super.clone() as ZigExecConfig<*> val myClone = super.clone() as ZigExecConfig<*>
myClone.workingDirectory = workingDirectory.clone() myClone.workingDirectory = workingDirectory.clone()
myClone.pty = pty.clone()
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return myClone as T return myClone as T
} }
open fun getConfigurables(): List<ZigConfigurable<*>> = listOf(workingDirectory, pty) open fun getConfigurables(): List<ZigConfigurable<*>> = listOf(workingDirectory)
} }

View file

@ -23,9 +23,11 @@
package com.falsepattern.zigbrains.project.execution.base package com.falsepattern.zigbrains.project.execution.base
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.ZigConsoleBuilder
import com.falsepattern.zigbrains.project.run.ZigProcessHandler import com.falsepattern.zigbrains.project.run.ZigProcessHandler
import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.shared.cli.startIPCAwareProcess
import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking
import com.falsepattern.zigbrains.shared.ipc.IPCUtil import com.falsepattern.zigbrains.shared.ipc.IPCUtil
import com.falsepattern.zigbrains.shared.ipc.ipc import com.falsepattern.zigbrains.shared.ipc.ipc
@ -35,11 +37,15 @@ import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.CommandLineState import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PtyCommandLine import com.intellij.execution.configurations.PtyCommandLine
import com.intellij.execution.filters.TextConsoleBuilder
import com.intellij.execution.process.ProcessHandler import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.platform.ide.progress.ModalTaskOwner import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.terminal.TerminalExecutionConsole
import com.intellij.util.system.OS
import kotlin.collections.contains
import kotlin.io.path.pathString import kotlin.io.path.pathString
abstract class ZigProfileState<T: ZigExecConfig<T>> ( abstract class ZigProfileState<T: ZigExecConfig<T>> (
@ -47,6 +53,10 @@ abstract class ZigProfileState<T: ZigExecConfig<T>> (
val configuration: T val configuration: T
): CommandLineState(environment) { ): CommandLineState(environment) {
init {
consoleBuilder = ZigConsoleBuilder(environment.project, true)
}
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
override fun startProcess(): ProcessHandler { override fun startProcess(): ProcessHandler {
return runModalOrBlocking({ModalTaskOwner.project(environment.project)}, {"ZigProfileState.startProcess"}) { return runModalOrBlocking({ModalTaskOwner.project(environment.project)}, {"ZigProfileState.startProcess"}) {
@ -57,7 +67,7 @@ abstract class ZigProfileState<T: ZigExecConfig<T>> (
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
suspend fun startProcessSuspend(): ProcessHandler { suspend fun startProcessSuspend(): ProcessHandler {
val toolchain = environment.project.zigProjectSettings.state.toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain")) val toolchain = environment.project.zigProjectSettings.state.toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain"))
return startProcess(getCommandLine(toolchain, false), environment.project) return getCommandLine(toolchain, false).startIPCAwareProcess(environment.project, emulateTerminal = true)
} }
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
@ -65,9 +75,7 @@ abstract class ZigProfileState<T: ZigExecConfig<T>> (
val workingDir = configuration.workingDirectory val workingDir = configuration.workingDirectory
val zigExePath = toolchain.zig.path() val zigExePath = toolchain.zig.path()
// TODO remove this check once JetBrains implements colored terminal in the debugger val cli = PtyCommandLine().withConsoleMode(false)
// https://youtrack.jetbrains.com/issue/CPP-11622/ANSI-color-codes-not-honored-in-Debug-Run-Configuration-output-window
val cli = if (configuration.emulateTerminal() && !debug) PtyCommandLine().withConsoleMode(true).withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) else GeneralCommandLine()
cli.withExePath(zigExePath.pathString) cli.withExePath(zigExePath.pathString)
workingDir.path?.let { cli.withWorkingDirectory(it) } workingDir.path?.let { cli.withWorkingDirectory(it) }
cli.withCharset(Charsets.UTF_8) cli.withCharset(Charsets.UTF_8)
@ -75,22 +83,3 @@ abstract class ZigProfileState<T: ZigExecConfig<T>> (
return configuration.patchCommandLine(cli) return configuration.patchCommandLine(cli)
} }
} }
@Throws(ExecutionException::class)
suspend fun executeCommandLine(commandLine: GeneralCommandLine, environment: ExecutionEnvironment): DefaultExecutionResult {
val handler = startProcess(commandLine, environment.project)
val console = BuildTextConsoleView(environment.project, false, emptyList())
console.attachToProcess(handler)
return DefaultExecutionResult(console, handler)
}
@Throws(ExecutionException::class)
suspend fun startProcess(commandLine: GeneralCommandLine, project: Project): ProcessHandler {
val ipc = IPCUtil.wrapWithIPC(commandLine)
val handler = ZigProcessHandler(ipc?.cli ?: commandLine)
ProcessTerminatedListener.attach(handler)
if (ipc != null) {
project.ipc?.launchWatcher(ipc, handler.process)
}
return handler
}

View file

@ -25,7 +25,6 @@ package com.falsepattern.zigbrains.project.execution.build
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.base.* import com.falsepattern.zigbrains.project.execution.base.*
import com.falsepattern.zigbrains.shared.ZBFeatures import com.falsepattern.zigbrains.shared.ZBFeatures
import com.falsepattern.zigbrains.shared.cli.coloredCliFlags
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.execution.configurations.ConfigurationFactory
@ -37,8 +36,6 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
private set private set
var extraArgs = ArgsConfigurable("compilerArgs", ZigBrainsBundle.message("exec.option.label.build.args")) var extraArgs = ArgsConfigurable("compilerArgs", ZigBrainsBundle.message("exec.option.label.build.args"))
private set private set
var colored = ColoredConfigurable("colored")
private set
var debugBuildSteps = ArgsConfigurable("debugBuildSteps", ZigBrainsBundle.message("exec.option.label.build.steps-debug")) var debugBuildSteps = ArgsConfigurable("debugBuildSteps", ZigBrainsBundle.message("exec.option.label.build.steps-debug"))
private set private set
var debugExtraArgs = ArgsConfigurable("debugCompilerArgs", ZigBrainsBundle.message("exec.option.label.build.args-debug")) var debugExtraArgs = ArgsConfigurable("debugCompilerArgs", ZigBrainsBundle.message("exec.option.label.build.args-debug"))
@ -54,7 +51,6 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
result.add("build") result.add("build")
val steps = if (debug) debugBuildSteps.argsSplit() else buildSteps.argsSplit() val steps = if (debug) debugBuildSteps.argsSplit() else buildSteps.argsSplit()
result.addAll(steps) result.addAll(steps)
result.addAll(coloredCliFlags(colored.value, debug))
result.addAll(if (debug) debugExtraArgs.argsSplit() else extraArgs.argsSplit()) result.addAll(if (debug) debugExtraArgs.argsSplit() else extraArgs.argsSplit())
return result return result
} }
@ -66,14 +62,13 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
val clone = super.clone() val clone = super.clone()
clone.buildSteps = buildSteps.clone() clone.buildSteps = buildSteps.clone()
clone.exeArgs = exeArgs.clone() clone.exeArgs = exeArgs.clone()
clone.colored = colored.clone()
clone.exePath = exePath.clone() clone.exePath = exePath.clone()
clone.exeArgs = exeArgs.clone() clone.exeArgs = exeArgs.clone()
return clone return clone
} }
override fun getConfigurables(): List<ZigConfigurable<*>> { override fun getConfigurables(): List<ZigConfigurable<*>> {
val baseCfg = super.getConfigurables() + listOf(buildSteps, extraArgs, colored) val baseCfg = super.getConfigurables() + listOf(buildSteps, extraArgs)
return if (ZBFeatures.debug()) { return if (ZBFeatures.debug()) {
baseCfg + listOf(debugBuildSteps, debugExtraArgs, exePath, exeArgs) baseCfg + listOf(debugBuildSteps, debugExtraArgs, exePath, exeArgs)
} else { } else {

View file

@ -24,7 +24,6 @@ package com.falsepattern.zigbrains.project.execution.run
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.base.* import com.falsepattern.zigbrains.project.execution.base.*
import com.falsepattern.zigbrains.shared.cli.coloredCliFlags
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.execution.configurations.ConfigurationFactory
@ -35,8 +34,6 @@ import kotlin.io.path.pathString
class ZigExecConfigRun(project: Project, factory: ConfigurationFactory): ZigExecConfig<ZigExecConfigRun>(project, factory, ZigBrainsBundle.message("exec.type.run.label")) { class ZigExecConfigRun(project: Project, factory: ConfigurationFactory): ZigExecConfig<ZigExecConfigRun>(project, factory, ZigBrainsBundle.message("exec.type.run.label")) {
var filePath = FilePathConfigurable("filePath", ZigBrainsBundle.message("exec.option.label.file-path")) var filePath = FilePathConfigurable("filePath", ZigBrainsBundle.message("exec.option.label.file-path"))
private set private set
var colored = ColoredConfigurable("colored")
private set
var optimization = OptimizationConfigurable("optimization") var optimization = OptimizationConfigurable("optimization")
private set private set
var compilerArgs = ArgsConfigurable("compilerArgs", ZigBrainsBundle.message("exec.option.label.compiler-args")) var compilerArgs = ArgsConfigurable("compilerArgs", ZigBrainsBundle.message("exec.option.label.compiler-args"))
@ -47,7 +44,6 @@ class ZigExecConfigRun(project: Project, factory: ConfigurationFactory): ZigExec
override suspend fun buildCommandLineArgs(debug: Boolean): List<String> { override suspend fun buildCommandLineArgs(debug: Boolean): List<String> {
val result = ArrayList<String>() val result = ArrayList<String>()
result.add(if (debug) "build-exe" else "run") result.add(if (debug) "build-exe" else "run")
result.addAll(coloredCliFlags(colored.value, debug))
result.add(filePath.path?.pathString ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig.empty-file-path"))) result.add(filePath.path?.pathString ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig.empty-file-path")))
if (!debug || optimization.forced) { if (!debug || optimization.forced) {
result.addAll(listOf("-O", optimization.level.name)) result.addAll(listOf("-O", optimization.level.name))
@ -66,7 +62,6 @@ class ZigExecConfigRun(project: Project, factory: ConfigurationFactory): ZigExec
override fun clone(): ZigExecConfigRun { override fun clone(): ZigExecConfigRun {
val clone = super.clone() val clone = super.clone()
clone.filePath = filePath.clone() clone.filePath = filePath.clone()
clone.colored = colored.clone()
clone.compilerArgs = compilerArgs.clone() clone.compilerArgs = compilerArgs.clone()
clone.optimization = optimization.clone() clone.optimization = optimization.clone()
clone.exeArgs = exeArgs.clone() clone.exeArgs = exeArgs.clone()
@ -74,7 +69,7 @@ class ZigExecConfigRun(project: Project, factory: ConfigurationFactory): ZigExec
} }
override fun getConfigurables(): List<ZigConfigurable<*>> { override fun getConfigurables(): List<ZigConfigurable<*>> {
return super.getConfigurables() + listOf(filePath, optimization, colored, compilerArgs, exeArgs) return super.getConfigurables() + listOf(filePath, optimization, compilerArgs, exeArgs)
} }
override fun getState(executor: Executor, environment: ExecutionEnvironment): ZigProfileState<ZigExecConfigRun> { override fun getState(executor: Executor, environment: ExecutionEnvironment): ZigProfileState<ZigExecConfigRun> {

View file

@ -24,7 +24,6 @@ package com.falsepattern.zigbrains.project.execution.test
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.base.* import com.falsepattern.zigbrains.project.execution.base.*
import com.falsepattern.zigbrains.shared.cli.coloredCliFlags
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.execution.configurations.ConfigurationFactory
@ -35,8 +34,6 @@ import kotlin.io.path.pathString
class ZigExecConfigTest(project: Project, factory: ConfigurationFactory): ZigExecConfig<ZigExecConfigTest>(project, factory, ZigBrainsBundle.message("exec.type.test.label")) { class ZigExecConfigTest(project: Project, factory: ConfigurationFactory): ZigExecConfig<ZigExecConfigTest>(project, factory, ZigBrainsBundle.message("exec.type.test.label")) {
var filePath = FilePathConfigurable("filePath", ZigBrainsBundle.message("exec.option.label.file-path")) var filePath = FilePathConfigurable("filePath", ZigBrainsBundle.message("exec.option.label.file-path"))
private set private set
var colored = ColoredConfigurable("colored")
private set
var optimization = OptimizationConfigurable("optimization") var optimization = OptimizationConfigurable("optimization")
private set private set
var compilerArgs = ArgsConfigurable("compilerArgs", ZigBrainsBundle.message("exec.option.label.compiler-args")) var compilerArgs = ArgsConfigurable("compilerArgs", ZigBrainsBundle.message("exec.option.label.compiler-args"))
@ -46,7 +43,6 @@ class ZigExecConfigTest(project: Project, factory: ConfigurationFactory): ZigExe
override suspend fun buildCommandLineArgs(debug: Boolean): List<String> { override suspend fun buildCommandLineArgs(debug: Boolean): List<String> {
val result = ArrayList<String>() val result = ArrayList<String>()
result.add("test") result.add("test")
result.addAll(coloredCliFlags(colored.value, debug))
result.add(filePath.path?.pathString ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig.empty-file-path"))) result.add(filePath.path?.pathString ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig.empty-file-path")))
if (!debug || optimization.forced) { if (!debug || optimization.forced) {
result.addAll(listOf("-O", optimization.level.name)) result.addAll(listOf("-O", optimization.level.name))
@ -64,14 +60,13 @@ class ZigExecConfigTest(project: Project, factory: ConfigurationFactory): ZigExe
override fun clone(): ZigExecConfigTest { override fun clone(): ZigExecConfigTest {
val clone = super.clone() val clone = super.clone()
clone.filePath = filePath.clone() clone.filePath = filePath.clone()
clone.colored = colored.clone()
clone.compilerArgs = compilerArgs.clone() clone.compilerArgs = compilerArgs.clone()
clone.optimization = optimization.clone() clone.optimization = optimization.clone()
return clone return clone
} }
override fun getConfigurables(): List<ZigConfigurable<*>> { override fun getConfigurables(): List<ZigConfigurable<*>> {
return super.getConfigurables() + listOf(filePath, optimization, colored, compilerArgs) return super.getConfigurables() + listOf(filePath, optimization, compilerArgs)
} }
override fun getState(executor: Executor, environment: ExecutionEnvironment): ZigProfileState<ZigExecConfigTest> { override fun getState(executor: Executor, environment: ExecutionEnvironment): ZigProfileState<ZigExecConfigTest> {

View file

@ -26,11 +26,12 @@ import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PtyCommandLine import com.intellij.execution.configurations.PtyCommandLine
import com.intellij.execution.process.AnsiEscapeDecoder.ColoredTextAcceptor import com.intellij.execution.process.AnsiEscapeDecoder.ColoredTextAcceptor
import com.intellij.execution.process.KillableColoredProcessHandler import com.intellij.execution.process.KillableColoredProcessHandler
import com.intellij.execution.process.KillableProcessHandler
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.pty4j.PtyProcess import com.pty4j.PtyProcess
import java.nio.charset.Charset import java.nio.charset.Charset
class ZigProcessHandler : KillableColoredProcessHandler, ColoredTextAcceptor { class ZigProcessHandler : KillableProcessHandler {
constructor(commandLine: GeneralCommandLine) : super(commandLine) { constructor(commandLine: GeneralCommandLine) : super(commandLine) {
setHasPty(commandLine is PtyCommandLine) setHasPty(commandLine is PtyCommandLine)
setShouldDestroyProcessRecursively(!hasPty()) setShouldDestroyProcessRecursively(!hasPty())
@ -40,57 +41,4 @@ class ZigProcessHandler : KillableColoredProcessHandler, ColoredTextAcceptor {
setHasPty(process is PtyProcess) setHasPty(process is PtyProcess)
setShouldDestroyProcessRecursively(!hasPty()) setShouldDestroyProcessRecursively(!hasPty())
} }
override fun coloredTextAvailable(text: String, attributes: Key<*>) {
super.coloredTextAvailable(text.translateVT100Escapes(), attributes)
}
}
private val VT100_CHARS = CharArray(256).apply {
this.fill(' ')
this[0x6A] = '┘'
this[0x6B] = '┐'
this[0x6C] = '┌'
this[0x6D] = '└'
this[0x6E] = '┼'
this[0x71] = '─'
this[0x74] = '├'
this[0x75] = '┤'
this[0x76] = '┴'
this[0x77] = '┬'
this[0x78] = '│'
}
private const val VT100_BEGIN_SEQ = "\u001B(0"
private const val VT100_END_SEQ = "\u001B(B"
private const val VT100_BEGIN_SEQ_LENGTH: Int = VT100_BEGIN_SEQ.length
private const val VT100_END_SEQ_LENGTH: Int = VT100_END_SEQ.length
private fun String.translateVT100Escapes(): String {
var offset = 0
val result = StringBuilder()
val textLength = length
while (offset < textLength) {
val startIndex = indexOf(VT100_BEGIN_SEQ, offset)
if (startIndex < 0) {
result.append(substring(offset, textLength).replace(VT100_END_SEQ, ""))
break
}
result.append(this, offset, startIndex)
val blockOffset = startIndex + VT100_BEGIN_SEQ_LENGTH
var endIndex = indexOf(VT100_END_SEQ, blockOffset)
if (endIndex < 0) {
endIndex = textLength
}
for (i in blockOffset until endIndex) {
val c = this[i].code
if (c >= 256) {
result.append(c)
} else {
result.append(VT100_CHARS[c])
}
}
offset = endIndex + VT100_END_SEQ_LENGTH
}
return result.toString()
} }

View file

@ -24,7 +24,6 @@ package com.falsepattern.zigbrains.project.run
import com.falsepattern.zigbrains.project.execution.base.ZigExecConfig import com.falsepattern.zigbrains.project.execution.base.ZigExecConfig
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.execution.base.executeCommandLine
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfile
@ -37,7 +36,7 @@ import com.intellij.openapi.application.ModalityState
class ZigRegularRunner: ZigProgramRunner<ZigProfileState<*>>(DefaultRunExecutor.EXECUTOR_ID) { class ZigRegularRunner: ZigProgramRunner<ZigProfileState<*>>(DefaultRunExecutor.EXECUTOR_ID) {
override suspend fun execute(state: ZigProfileState<*>, toolchain: AbstractZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? { override suspend fun execute(state: ZigProfileState<*>, toolchain: AbstractZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? {
val cli = state.getCommandLine(toolchain, false) val cli = state.getCommandLine(toolchain, false)
val exec = executeCommandLine(cli, environment) val exec = state.execute(environment.executor, this)
return withEDTContext(ModalityState.any()) { return withEDTContext(ModalityState.any()) {
val runContentBuilder = RunContentBuilder(exec, environment) val runContentBuilder = RunContentBuilder(exec, environment)
runContentBuilder.showRunContent(null) runContentBuilder.showRunContent(null)

View file

@ -23,13 +23,18 @@
package com.falsepattern.zigbrains.shared.cli package com.falsepattern.zigbrains.shared.cli
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.run.ZigProcessHandler
import com.falsepattern.zigbrains.shared.ipc.IPCUtil import com.falsepattern.zigbrains.shared.ipc.IPCUtil
import com.falsepattern.zigbrains.shared.ipc.ipc import com.falsepattern.zigbrains.shared.ipc.ipc
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessOutput import com.intellij.execution.process.ProcessOutput
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.openapi.options.ConfigurationException import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.util.io.awaitExit import com.intellij.util.io.awaitExit
import com.intellij.util.system.OS
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -108,14 +113,6 @@ fun translateCommandline(toProcess: String): List<String> {
return result return result
} }
fun coloredCliFlags(colored: Boolean, debug: Boolean): List<String> {
return if (debug) {
emptyList()
} else {
listOf("--color", if (colored) "on" else "off")
}
}
fun createCommandLineSafe( fun createCommandLineSafe(
workingDirectory: Path?, workingDirectory: Path?,
exe: Path, exe: Path,
@ -133,14 +130,27 @@ fun createCommandLineSafe(
return Result.success(cli) return Result.success(cli)
} }
suspend fun GeneralCommandLine.call(timeoutMillis: Long = Long.MAX_VALUE, ipcProject: Project? = null): Result<ProcessOutput> { @Throws(ExecutionException::class)
val ipc = if (ipcProject != null) IPCUtil.wrapWithIPC(this) else null suspend fun GeneralCommandLine.startIPCAwareProcess(project: Project?, emulateTerminal: Boolean = false): ZigProcessHandler {
val ipc = if (project != null && !emulateTerminal) IPCUtil.wrapWithIPC(this) else null
val cli = ipc?.cli ?: this val cli = ipc?.cli ?: this
val (process, exitCode) = withContext(Dispatchers.IO) { if (emulateTerminal && OS.CURRENT != OS.Windows && !cli.environment.contains("TERM")) {
val process = cli.createProcess() cli.withEnvironment("TERM", "xterm-256color")
if (ipc != null) {
ipcProject!!.ipc?.launchWatcher(ipc, process)
} }
val handler = ZigProcessHandler(cli)
ProcessTerminatedListener.attach(handler)
if (ipc != null) {
project!!.ipc?.launchWatcher(ipc, handler.process)
}
return handler
}
suspend fun GeneralCommandLine.call(timeoutMillis: Long = Long.MAX_VALUE, ipcProject: Project? = null): Result<ProcessOutput> {
val (process, exitCode) = withContext(Dispatchers.IO) {
val handler = startIPCAwareProcess(ipcProject)
val process = handler.process
val exit = withTimeoutOrNull(timeoutMillis) { val exit = withTimeoutOrNull(timeoutMillis) {
process.awaitExit() process.awaitExit()
} }