Skip to content

Commit c3fc404

Browse files
committed
feat: configurable screenshot detection
1 parent 163758a commit c3fc404

File tree

9 files changed

+390
-30
lines changed

9 files changed

+390
-30
lines changed

src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationComponent.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class ConfigurationComponent {
2929
private final JBTextField temperatureField;
3030
private final CodeCompletionConfigurationForm codeCompletionForm;
3131
private final ChatCompletionConfigurationForm chatCompletionForm;
32+
private final ScreenshotConfigurationForm screenshotForm;
3233

3334
public ConfigurationComponent(
3435
Disposable parentDisposable,
@@ -74,6 +75,8 @@ public void changedUpdate(DocumentEvent e) {
7475

7576
codeCompletionForm = new CodeCompletionConfigurationForm();
7677
chatCompletionForm = new ChatCompletionConfigurationForm();
78+
screenshotForm = new ScreenshotConfigurationForm();
79+
screenshotForm.loadState(configuration.getScreenshotWatchPaths());
7780

7881
mainPanel = FormBuilder.createFormBuilder()
7982
.addComponent(checkForPluginUpdatesCheckBox)
@@ -84,6 +87,9 @@ public void changedUpdate(DocumentEvent e) {
8487
.addComponent(new TitledSeparator(
8588
CodeGPTBundle.get("configurationConfigurable.section.assistant.title")))
8689
.addComponent(createAssistantConfigurationForm())
90+
.addComponent(new TitledSeparator(
91+
CodeGPTBundle.get("configurationConfigurable.section.screenshots.title")))
92+
.addComponent(screenshotForm.createPanel())
8793
.addComponent(new TitledSeparator(
8894
CodeGPTBundle.get("configurationConfigurable.section.codeCompletion.title")))
8995
.addComponent(codeCompletionForm.createPanel())
@@ -108,6 +114,11 @@ public ConfigurationSettingsState getCurrentFormState() {
108114
state.setAutoFormattingEnabled(autoFormattingCheckBox.isSelected());
109115
state.setCodeCompletionSettings(codeCompletionForm.getFormState());
110116
state.setChatCompletionSettings(chatCompletionForm.getFormState());
117+
118+
var screenshotPaths = screenshotForm.getState();
119+
state.getScreenshotWatchPaths().clear();
120+
state.getScreenshotWatchPaths().addAll(screenshotPaths);
121+
111122
return state;
112123
}
113124

@@ -121,6 +132,7 @@ public void resetForm() {
121132
autoFormattingCheckBox.setSelected(configuration.getAutoFormattingEnabled());
122133
codeCompletionForm.resetForm(configuration.getCodeCompletionSettings());
123134
chatCompletionForm.resetForm(configuration.getChatCompletionSettings());
135+
screenshotForm.loadState(configuration.getScreenshotWatchPaths());
124136
}
125137

126138
// Formatted keys are not referenced in the messages bundle file

src/main/kotlin/ee/carlrobert/codegpt/CodeGPTProjectActivity.kt

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,33 +8,49 @@ import com.intellij.openapi.project.Project
88
import com.intellij.openapi.startup.ProjectActivity
99
import ee.carlrobert.codegpt.actions.editor.EditorActionsUtil
1010
import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings
11+
import ee.carlrobert.codegpt.settings.configuration.ScreenshotPathDetector
1112
import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTService
1213
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.AttachImageNotifier
1314
import ee.carlrobert.codegpt.ui.OverlayUtil
15+
import com.intellij.openapi.diagnostic.thisLogger
1416
import java.nio.file.Path
1517
import java.nio.file.Paths
1618
import kotlin.io.path.absolutePathString
1719

1820
class CodeGPTProjectActivity : ProjectActivity {
1921

20-
private val watchExtensions = setOf("jpg", "jpeg", "png")
22+
private val logger = thisLogger()
2123

2224
override suspend fun execute(project: Project) {
2325
EditorActionsUtil.refreshActions()
24-
2526
project.service<CodeGPTService>().syncUserDetailsAsync()
2627

2728
if (!ApplicationManager.getApplication().isUnitTestMode
2829
&& service<ConfigurationSettings>().state.checkForNewScreenshots
2930
) {
30-
val desktopPath = Paths.get(System.getProperty("user.home"), "Desktop")
31-
project.service<FileWatcher>().watch(desktopPath) {
32-
if (watchExtensions.contains(getFileExtension(it))) {
33-
showImageAttachmentNotification(
34-
project,
35-
desktopPath.resolve(it).absolutePathString()
36-
)
31+
val configurationState = service<ConfigurationSettings>().state
32+
val watchPaths = configurationState.screenshotWatchPaths.ifEmpty {
33+
ScreenshotPathDetector.getDefaultPaths()
34+
}
35+
val watchExtensions = ScreenshotPathDetector.getDefaultFileExtensions().toSet()
36+
logger.debug("Screenshot watch configuration - paths: $watchPaths, extensions: $watchExtensions")
37+
val validPaths = watchPaths.filter { ScreenshotPathDetector.isValidWatchPath(it) }
38+
logger.debug("Valid watch paths after filtering: $validPaths")
39+
if (validPaths.isNotEmpty()) {
40+
logger.info("Starting screenshot file watching for ${validPaths.size} paths")
41+
project.service<FileWatcher>().watchMultiplePaths(validPaths) { fileName, watchPath ->
42+
val fileExtension = getFileExtension(fileName)
43+
logger.trace("File detected: fileName=$fileName, extension='$fileExtension', watchPath=$watchPath")
44+
if (watchExtensions.contains(fileExtension)) {
45+
val fullPath = Paths.get(watchPath).resolve(fileName).absolutePathString()
46+
logger.info("New screenshot file created: $fullPath (extension='$fileExtension')")
47+
showImageAttachmentNotification(project, fullPath)
48+
} else {
49+
logger.trace("File extension '$fileExtension' not in watch list: $watchExtensions")
50+
}
3751
}
52+
} else {
53+
logger.warn("No valid screenshot watch paths found - screenshot detection disabled")
3854
}
3955
}
4056
}

src/main/kotlin/ee/carlrobert/codegpt/FileWatcher.kt

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,84 @@ package ee.carlrobert.codegpt
22

33
import com.intellij.openapi.Disposable
44
import com.intellij.openapi.components.Service
5+
import com.intellij.openapi.diagnostic.thisLogger
56
import java.nio.file.FileSystems
67
import java.nio.file.Path
8+
import java.nio.file.Paths
79
import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE
10+
import java.nio.file.WatchService
811
import kotlin.concurrent.thread
912
import kotlin.io.path.exists
1013

11-
1214
@Service(Service.Level.PROJECT)
1315
class FileWatcher : Disposable {
1416

15-
private var fileMonitor: Thread? = null
17+
private val watchServices = mutableListOf<WatchService>()
18+
private val fileMonitors = mutableListOf<Thread>()
19+
private val logger = thisLogger()
20+
21+
fun watchMultiplePaths(pathsToWatch: List<String>, onFileCreated: (Path, String) -> Unit) {
22+
dispose()
1623

17-
fun watch(pathToWatch: Path, onFileCreated: (Path) -> Unit) {
18-
fileMonitor = pathToWatch.takeIf { it.exists() }?.let {
24+
pathsToWatch.forEach { pathString ->
1925
try {
20-
FileSystems.getDefault().newWatchService().also {
21-
pathToWatch.register(it, ENTRY_CREATE) // watch for new files
22-
}
23-
} catch (e: Exception) {
24-
null // WatchService or registration failed
25-
}
26-
}?.let { watchService ->
27-
thread {
28-
try {
29-
generateSequence { watchService.take() }.forEach { key ->
30-
key.pollEvents().forEach { onFileCreated(it.context() as Path) }
31-
key.reset()
26+
val path = Paths.get(pathString)
27+
if (path.exists()) {
28+
val watchService = FileSystems.getDefault().newWatchService()
29+
path.register(watchService, ENTRY_CREATE)
30+
watchServices.add(watchService)
31+
logger.debug("Successfully registered watch service for path: $pathString (absolute: ${path.toAbsolutePath()})")
32+
33+
val monitor = thread {
34+
try {
35+
logger.debug("File watch monitor thread started for path: $pathString")
36+
generateSequence { watchService.take() }.forEach { key ->
37+
logger.trace("Watch event received for path: $pathString")
38+
key.pollEvents().forEach { event ->
39+
val fileName = event.context() as Path
40+
val fullPath = path.resolve(fileName)
41+
logger.debug("File event detected: ${event.kind()} - fileName=$fileName, fullPath=$fullPath")
42+
onFileCreated(fileName, pathString)
43+
}
44+
val resetResult = key.reset()
45+
if (!resetResult) {
46+
logger.warn("Watch key reset failed for path: $pathString - watch may have become invalid")
47+
}
48+
}
49+
} catch (e: InterruptedException) {
50+
logger.debug("File watch monitor thread interrupted for path: $pathString")
51+
Thread.currentThread().interrupt()
52+
} catch (e: Exception) {
53+
logger.warn("Error in file watcher for path: $pathString", e)
54+
} finally {
55+
logger.debug("File watch monitor thread stopped for path: $pathString")
56+
}
3257
}
33-
} catch (e: InterruptedException) {
34-
Thread.currentThread().interrupt()
58+
fileMonitors.add(monitor)
59+
60+
logger.info("Started watching path: $pathString")
61+
} else {
62+
logger.warn("Path does not exist or is not accessible: $pathString")
3563
}
64+
} catch (e: Exception) {
65+
logger.warn("Failed to set up watcher for path: $pathString", e)
3666
}
3767
}
3868
}
3969

4070
override fun dispose() {
41-
fileMonitor?.interrupt()
71+
logger.debug("Disposing FileWatcher - stopping ${fileMonitors.size} monitor threads and ${watchServices.size} watch services")
72+
fileMonitors.forEach { it.interrupt() }
73+
fileMonitors.clear()
74+
75+
watchServices.forEach { watchService ->
76+
try {
77+
watchService.close()
78+
} catch (e: Exception) {
79+
logger.warn("Error closing watch service", e)
80+
}
81+
}
82+
watchServices.clear()
83+
logger.debug("FileWatcher disposal completed")
4284
}
4385
}

src/main/kotlin/ee/carlrobert/codegpt/settings/configuration/ConfigurationSettings.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,22 @@ class ConfigurationSettingsState : BaseState() {
2727
var temperature by property(0.1f) { max(0f, min(1f, it)) }
2828
var checkForPluginUpdates by property(true)
2929
var checkForNewScreenshots by property(true)
30+
var screenshotWatchPaths by list<String>()
3031
var ignoreGitCommitTokenLimit by property(false)
3132
var methodNameGenerationEnabled by property(true)
3233
var captureCompileErrors by property(true)
3334
var autoFormattingEnabled by property(true)
3435
var tableData by map<String, String>()
3536
var chatCompletionSettings by property(ChatCompletionSettingsState())
3637
var codeCompletionSettings by property(CodeCompletionSettingsState())
38+
var myAwesomeFeatureEnabled by property(false)
3739

3840
init {
3941
tableData.putAll(EditorActionsUtil.DEFAULT_ACTIONS)
42+
43+
if (screenshotWatchPaths.isEmpty()) {
44+
screenshotWatchPaths.addAll(ScreenshotPathDetector.getDefaultPaths())
45+
}
4046
}
4147
}
4248

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package ee.carlrobert.codegpt.settings.configuration
2+
3+
import com.intellij.openapi.ui.DialogWrapper
4+
import com.intellij.openapi.ui.TextFieldWithBrowseButton
5+
import com.intellij.util.ui.FormBuilder
6+
import com.intellij.util.ui.JBUI
7+
import ee.carlrobert.codegpt.CodeGPTBundle
8+
import java.awt.Dimension
9+
import javax.swing.JComponent
10+
11+
class PathInputDialog(
12+
title: String,
13+
private val textField: TextFieldWithBrowseButton
14+
) : DialogWrapper(true) {
15+
16+
init {
17+
this.title = title
18+
init()
19+
}
20+
21+
override fun createCenterPanel(): JComponent {
22+
val panel = FormBuilder.createFormBuilder()
23+
.addLabeledComponent(
24+
CodeGPTBundle.get("configurationConfigurable.screenshotPaths.dialog.path.label"),
25+
textField,
26+
true
27+
)
28+
.panel
29+
panel.preferredSize = Dimension(JBUI.scale(500), panel.preferredSize.height)
30+
return panel
31+
}
32+
33+
override fun getPreferredFocusedComponent(): JComponent = textField
34+
}

0 commit comments

Comments
 (0)