24 KiB
Garnet Integration into Amethyst - Project Notes & Learnings
Quick Reference
| Item | Value |
|---|---|
| Garnet repo | https://github.com/retrnull/garnet |
| Amethyst repo | https://github.com/vitorpamplona/amethyst |
| Garnet base amethyst commit | 175b79b29 (April 2024) |
| Garnet fork date | ~July 10, 2024 |
| Latest amethyst commit | a4310317a (May 2026) |
| Commits since fork | ~9,330 |
| Garnet version | v0.3.0 |
| Monero source | https://github.com/retrnull/monero (branch: release-v0.18.3.3-garnet) |
| Current status | Core infrastructure ported (21 new files, 6 modified). UI layer + wiring + build verification remaining. |
What was accomplished (this session)
- Extracted all monero model classes from Garnet Kotlin code
- Adapted imports for new KMP quartz package structure (
nip01Core.*) - Created wallet model classes (Wallet, PendingTransaction, Subaddress, etc.)
- Created WalletService (fully implemented from Garnet source)
- Created MoneroWalletListener interface
- Created MoneroTipEvent in quartz (
moneroTippackage) - Created TipSplitSetup utility in quartz (
moneroTippackage) - Created TransactionPriority enum
- Created TipHandler and TipEventDataSource (stubs)
- Created MoneroDataSource (stub with flows)
- Added monero wallet fields to Account.kt (spend key, seed, restore height, password flows)
- Added monero wallet methods to Account.kt (startMonero, stopMonero, balance, address, generateSpendKey)
- Added
moneroAddress()method to User model in commons - Added
moneroAddressFromAbout()method to UserMetadataCache in commons - Added
moneroAddressIsValid()utility function in commons - Added EventFactory registration for MoneroTipEvent (kind 1814)
- Added monero.xml icon to resources
- Added monero-specific strings to strings.xml
- Added monero JNI bridge files (monerujo.cpp, monerujo.h, wallet2_api.h)
- Updated amethyst/build.gradle.kts for monero dependencies
What's remaining (next session)
- Wire up MoneroDataSource setMoneroService() to connect WalletService flows
- Implement AccountViewModel monero state management
- Port the UI layer (~19 files of Compose screens)
- Add NIP-69 monero address support
- Build and verify (
./gradlew assembleFdroidDebug) - Test monero wallet functionality
What Worked ✓
21 New Files Created
| # | File | Description |
|---|---|---|
| 1 | amethyst/src/main/java/.../model/MoneroWalletListener.kt |
JNI wallet listener callback interface |
| 2 | amethyst/src/main/java/.../model/PendingTransaction.kt |
JNI pending transaction wrapper |
| 3 | amethyst/src/main/java/.../model/ProofInfo.kt |
Proof verification data class |
| 4 | amethyst/src/main/java/.../model/Subaddress.kt |
Monero subaddress model |
| 5 | amethyst/src/main/java/.../model/Transaction.kt |
Monero transaction model |
| 6 | amethyst/src/main/java/.../model/TransactionInfo.kt |
Transaction info wrapper |
| 7 | amethyst/src/main/java/.../model/TransactionHistory.kt |
Transaction history model |
| 8 | amethyst/src/main/java/.../model/Transfer.kt |
Transfer data class |
| 9 | amethyst/src/main/java/.../model/Wallet.kt |
JNI Wallet wrapper (all native methods) |
| 10 | amethyst/src/main/java/.../model/WalletManager.kt |
Wallet factory + native bridge utilities |
| 11 | amethyst/src/main/java/.../service/MoneroDataSource.kt |
Data source stub with Flow wrappers |
| 12 | amethyst/src/main/java/.../service/TipEventDataSource.kt |
Tip event lifecycle stubs |
| 13 | amethyst/src/main/java/.../service/TipHandler.kt |
Tip processing logic (partial) |
| 14 | amethyst/src/main/java/.../service/WalletService.kt |
Android Service + all wallet operations |
| 15 | amethyst/src/main/java/.../ui/screen/loggedIn/TransactionPriority.kt |
Transaction priority enum |
| 16 | amethyst/src/main/res/drawable/monero.xml |
Monero icon resource |
| 17 | quartz/src/commonMain/kotlin/.../moneroTip/MoneroTipEvent.kt |
Nostr tip event (kind 1814) |
| 18 | quartz/src/commonMain/kotlin/.../moneroTip/TipSplitSetup.kt |
Tip split utilities + monero tag parsing |
| 19 | amethyst/src/main/cpp/monerujo.cpp |
JNI bridge C++ implementation |
| 20 | amethyst/src/main/cpp/monerujo.h |
JNI bridge header |
| 21 | amethyst/src/main/cpp/wallet2_api.h |
Monero wallet API header |
6 Modified Files
| # | File | Changes |
|---|---|---|
| 1 | amethyst/src/main/java/.../model/Account.kt |
Added monero wallet fields + methods |
| 2 | amethyst/src/main/res/values/strings.xml |
Added monero string resources |
| 3 | commons/src/commonMain/kotlin/.../commons/model/User.kt |
Added moneroAddress(), moneroAddressIsValid() |
| 4 | commons/src/commonMain/kotlin/.../commons/model/nip01Core/UserMetadataCache.kt |
Added moneroAddressFromAbout() |
| 5 | quartz/src/commonMain/kotlin/.../utils/EventFactory.kt |
Registered MoneroTipEvent (kind 1814), import |
| 6 | amethyst/build.gradle.kts |
Added monero module/dependencies |
Current File Structure with Exact Import Paths
This is the most critical section. All Garnet-era import paths were different because the quartz module was not yet Kotlin Multiplatform. Here are the exact current paths every imported type should use:
Quartz (MoneroEvent) Import Paths
CRITICAL: These are DIFFERENT from Garnet source.
| Type | New import path |
|---|---|
| Event base class | com.vitorpamplona.quartz.nip01Core.core.Event |
| HexKey | com.vitorpamplona.quartz.nip01Core.core.HexKey |
| IEvent | com.vitorpamplona.quartz.nip01Core.core.IEvent |
| AddressableEvent | com.vitorpamplona.quartz.nip01Core.core.AddressableEvent |
| JsonMapper | com.vitorpamplona.quartz.nip01Core.core.JsonMapper |
| OptimizedSerializable | com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable |
| TagArray | com.vitorpamplona.quartz.nip01Core.core.TagArray |
| KeyPair | com.vitorpamplona.quartz.nip01Core.crypto.KeyPair (was com.vitorpamplona.quartz.nip01Core.enc.KeyPair in garnet) |
| NostrSignerInternal | com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal (was ...signs.NostrSignerInternal) |
| NostrSigner | com.vitorpamplona.quartz.nip01Core.signers.NostrSigner |
| EventTemplate | com.vitorpamplona.quartz.nip01Core.signers.EventTemplate |
| ETag | com.vitorpamplona.quartz.nip01Core.tags.events.ETag |
| PTag | com.vitorpamplona.quartz.nip01Core.tags.people.PTag |
| EventHintProvider | com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider |
| PubKeyHintProvider | com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider |
| NormalizedRelayUrl | com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl |
Package Locations
| Type | Location |
|---|---|
| Note class | com.vitorpamplona.amethyst.commons.model.Note (was amethyst-only before, now in commons) |
| User class | com.vitorpamplona.amethyst.commons.model.User (was amethyst-only before, now in commons) |
| LocalCache | com.vitorpamplona.amethyst.model.LocalCache (stays in amethyst) |
| MoneroTipEvent | com.vitorpamplona.quartz.moneroTip.MoneroTipEvent |
| TipSplitSetup | com.vitorpamplona.quartz.moneroTip.TipSplitSetup |
Old Garnet → New Amethyst Mapping
Many Garnet-era packages have been renamed. Common mappings:
| Old (Garnet/2024) | New (2026) |
|---|---|
com.vitorpamplona.quartz.events.EncryptedContentUtils |
com.vitorpamplona.quartz.nip01Core.crypto.EventHasher |
com.vitorpamplona.quartz.events.* |
com.vitorpamplona.quartz.nip01Core.* (various subpackages) |
com.vitorpamplona.quartz.events.signs.* |
com.vitorpamplona.quartz.nip01Core.signers.* |
com.vitorpamplona.quartz.events.enc.* |
com.vitorpamplona.quartz.nip01Core.crypto.* |
com.vitorpamplona.quartz.events.crypto.* |
com.vitorpamplona.quartz.nip01Core.crypto.* |
commons.model.* (amethyst) |
commons.model.* (commons-commonMain) |
NostrSigner API Change (CRITICAL)
The signing API changed significantly. Garnet-era code uses callback-based signing. Current Amethyst uses coroutine-based signing.
Old (callback, Garnet-era)
// Garnet's NostrSigner callback style
signer.sign(eventBuilder, onReady: (Event) -> Unit)
New (coroutine, current)
// Current Amethyst coroutine style
val event = signer.sign(template)
// or
val event = signer.sign(createdAt, kind, tags, content)
NostrSignerInternal signing with createdAt
In current code, NostrSignerInternal.sign() with createdAt uses the vararg tags variant:
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
val event: MoneroTipEvent = NostrSignerInternal(KeyPair())
.sign(createdAt = TimeUtils.now(), kind = MoneroTipEvent.KIND, tags = tags.toTypedArray(), content = content)
See LnZapRequestEvent.kt:130 for a real example of creating an anonymous signer for private zaps.
LocalCache.consume() Signature
The justConsume() method in LocalCache takes 3 parameters:
fun justConsume(event: Event, relay: NormalizedRelayUrl?, isOwn: Boolean): Boolean
| Parameter | Description |
|---|---|
event |
The Nostr event to consume |
relay |
The relay it came from (null if from client) |
isOwn |
Whether this is the current account's own event |
Common usage patterns:
// Consume from relay, not own event
cache.justConsume(event, relay, false)
// Consume own event (no relay)
cache.justConsumeMyOwnEvent(event) // convenience wrapper, same as justConsume(event, null, true)
// Consume gift wrap / inner event from push
cache.justConsume(innerGift, null, false)
// Consume decrypted inner event from Marmot
cache.justConsume(innerEvent, null, true)
User Metadata Access Pattern
User metadata in commons.model.User is accessed via lazy-initialized caches:
// UserMetadataCache
val cache = user.metadata() // lazy init
val flow: StateFlow<UserInfo?> = cache.flow
val name = cache.bestName() // String?
val picture = cache.profilePicture() // String?
val lnAddress = cache.lnAddress() // String?
// For monero:
val moneroAddr = cache.moneroAddressFromAbout() // String?
In User model, the moneroAddress() method tries nip69Address() first (not yet implemented), then falls back to moneroAddressFromAbout():
fun User.moneroAddress(): String? {
val nip69 = nip69Address() // NIP-69 type-0 event, stub returns null
if (nip69 != null) return nip69
return metadataOrNull()?.moneroAddressFromAbout() // regex from about field
}
The regex in moneroAddressFromAbout(): \b[48]\w{93}\b — matches standard Monero addresses (4xxxx or 8xxxx, 95 chars total).
Amethyst.instance Reference Pattern
The global Amethyst singleton uses these accessors:
import com.vitorpamplona.amethyst.Amethyst
val context = Amethyst.instance.appContext // NOT applicationContext
val customScope = Amethyst.instance.customScope // UI coroutine scope
val ioScope = Amethyst.instance.applicationIOScope // IO coroutine scope
Important: Use appContext, NOT applicationContext. Garnet-era code used applicationContext which no longer compiles.
See WalletService.kt:68-80 for the correct pattern:
Amethyst.instance.appContext.getString(R.string.wallet_status_opening)
Amethyst.instance.applicationIOScope // used for scope.launch in wallet listener
TransactionPriority Enum
Defined at: amethyst/src/main/java/.../ui/screen/loggedIn/TransactionPriority.kt
enum class TransactionPriority(val value: Int) {
UNIMPORTANT(0),
SLOW(1),
MEDIUM(2),
FAST(3),
ESTIMATED(4),
}
Used everywhere Monero transactions are created:
wallet.createTransaction(
destination = address,
amount = amount,
priority = TransactionPriority.UNIMPORTANT // passes priority.ordinal to JNI
)
WalletService Public API
WalletService (Android Service) implements the full wallet. All public methods:
| Method | Return | Description |
|---|---|---|
loadWallet(name, password, spendKey="", daemonAddress, ...) |
Wallet |
Open or create wallet |
load(name, password, spendKey="", daemonAddress, ...) |
Wallet |
Same, with checkNotInMainThread |
connectToDaemon(address, username, password, proxy, restoreHeight, walletPassword) |
Wallet.Status? |
Connect to daemon |
connect(address, username, password, proxy, isCreation, restoreHeight, walletPassword) |
Wallet.Status? |
Same, check not on main thread |
sendTransaction(destination, amount, priority) |
PendingTransaction.Status? |
Send to single address |
sendTransactionMultDest(destinations[], amounts[], priority) |
PendingTransaction? |
Multi-destination |
getTxProof(txId, destination, message) |
Proof? |
Generate transaction proof |
checkTxProof(txId, address, message, signature) |
ProofInfo? |
Verify a transaction proof |
setProxy(proxy) |
Boolean? |
Set network proxy |
storeWallet() |
Wallet.Status? |
Persist wallet to disk |
closeWallet() |
Unit | Close the current wallet |
newSubaddress(accountIndex, label) |
Subaddress? |
Generate new subaddress |
lastSubaddress(accountIndex) |
Subaddress? |
Get last subaddress |
listAddresses() |
List<Subaddress>? |
List all subaddresses |
setSubaddressLabel(index, label) |
Wallet.Status? |
Label a subaddress |
seedWithPassphrase(passphrase) |
String? |
Get wallet seed |
isAddressValid(address) |
Boolean? |
Validate address format |
getRestoreHeight() |
Long? |
Get restore height |
getHistory() |
TransactionHistory? |
Get transaction history |
setUserNote(txId, note) |
Boolean? |
Attach user note to transaction |
estimateTransactionFee(destinations[], amounts[], priority) |
Long? |
Estimate fee in picoMonero |
getBalance |
Long |
Property: total balance |
getLockedBalance |
Long |
Property: locked (unconfirmed) balance |
walletHeight |
Long |
Property: wallet blockchain height |
daemonHeight |
Long |
Property: daemon blockchain height |
address |
String? |
Property: primary address |
connectionStatus |
Wallet.ConnectionStatus |
Property: DISCONNECTED/CONNECTED/WRONG_VERSION |
State flows (MutableStateFlow → StateFlow):
statusStateFlow: StateFlow<WalletStatus>balanceStateFlow: StateFlow<Long>lockedBalanceStateFlow: StateFlow<Long>walletHeightStateFlow: StateFlow<Long>daemonHeightStateFlow: StateFlow<Long>connectionStatusStateFlow: StateFlow<Wallet.ConnectionStatus>transactions: StateFlow<List<TransactionInfo>>
MoneroDataSource Stub — Current State
File: amethyst/src/main/java/.../service/MoneroDataSource.kt
object MoneroDataSource {
@Volatile
private var flows: MoneroServiceFlows = MoneroServiceFlows()
fun status(): Flow<*> = flows.status
fun connectionStatus(): Flow<*> = flows.connectionStatus
fun walletHeight(): Flow<Long> = flows.walletHeight
fun daemonHeight(): Flow<Long> = flows.daemonHeight
fun balance(): Flow<Long> = flows.balance
fun lockedBalance(): Flow<Long> = flows.lockedBalance
fun transactions(): Flow<List<TransactionInfo>> = flows.transactions
fun setMoneroService(service: WalletService) {
// Wire up the service flows here when fully implemented
}
}
The setMoneroService() stub currently does nothing. It must be wired up to forward WalletService's StateFlows into MoneroServiceFlows. This is likely done from AccountViewModel when the monero settings screen confirms the wallet.
What Next Session Must Do
Priority A: Wire Up MoneroDataSource
- Implement
MoneroDataSource.setMoneroService()to wire WalletService flows into MoneroServiceFlows - Create a
MoneroWalletConnectionHandleror wire fromAccountViewModelwhen user confirms monero wallet setup - Make MoneroDataSource the single source of truth for UI consumption
Priority B: Implement Account Methods That Need WalletService
The Account model already has the basic monero methods (startMonero, stopMonero, getBalance, etc.). What remains:
startMonero()needs to callWalletService.loadWallet()via a bound service connection- Wire
bindWalletService()to connectMoneroDataSource.setMoneroService() - Implement
stopMonero()to unbind service and clean up - Make
generateMoneroSpendKey()work with key derivation from Nostr key
Priority C: Port the UI Layer (~19 Files)
These UI files from Garnet need to be ported/adapted:
| # | Garnet UI File (old) | Target in Amethyst |
|---|---|---|
| 1 | .../ui/screen/loggedIn/settings/MoneroWalletSettingsScreen.kt |
amethyst/.../loggedIn/settings/MoneroWalletSettingsScreen.kt |
| 2 | .../ui/screen/loggedIn/settings/MoneroWalletViewModel.kt |
amethyst/.../loggedIn/settings/MoneroWalletViewModel.kt |
| 3 | .../ui/screen/loggedIn/settings/MoneroSendScreen.kt |
amethyst/.../loggedIn/settings/MoneroSendScreen.kt |
| 4 | .../ui/screen/loggedIn/settings/MoneroSendViewModel.kt |
amethyst/.../loggedIn/settings/MoneroSendViewModel.kt |
| 5 | .../ui/screen/loggedIn/monero/MoneroWalletScreen.kt |
amethyst/.../monero/MoneroWalletScreen.kt |
| 6 | .../ui/screen/loggedIn/monero/MoneroWalletViewModel.kt |
amethyst/.../monero/MoneroWalletViewModel.kt |
| 7 | .../ui/note/types/MoneroTipCompose.kt |
amethyst/.../note/types/MoneroTipCompose.kt |
| 8 | .../ui/screen/loggedIn/note/MoneroTipComposeViewModel.kt |
amethyst/.../note/MoneroTipComposeViewModel.kt |
| 9 | .../ui/screen/loggedIn/MoneroTippingHistoryScreen.kt |
amethyst/.../MoneroTippingHistoryScreen.kt |
| 10 | .../ui/screen/loggedIn/MoneroTippingHistoryViewModel.kt |
amethyst/.../MoneroTippingHistoryViewModel.kt |
| 11 | .../ui/screen/loggedIn/MoneroWalletInfoScreen.kt |
amethyst/.../MoneroWalletInfoScreen.kt |
| 12 | .../ui/screen/loggedIn/MoneroWalletInfoViewModel.kt |
amethyst/.../MoneroWalletInfoViewModel.kt |
| 13 | .../ui/screen/loggedIn/MoneroSendScreen.kt |
amethyst/.../MoneroSendScreen.kt |
| 14 | .../ui/screen/loggedIn/MoneroSendScreenViewModel.kt |
amethyst/.../MoneroSendScreenViewModel.kt |
| 15 | .../ui/components/MoneroAddressText.kt |
amethyst/.../components/MoneroAddressText.kt |
| 16 | .../ui/screen/loggedIn/MoneroReceiveScreen.kt |
amethyst/.../MoneroReceiveScreen.kt |
| 17 | .../ui/screen/loggedIn/MoneroReceiveViewModel.kt |
amethyst/.../MoneroReceiveViewModel.kt |
| 18 | .../ui/screen/loggedIn/MoneroAddressScreen.kt |
amethyst/.../MoneroAddressScreen.kt |
| 19 | .../ui/screen/loggedIn/MoneroAddressViewModel.kt |
amethyst/.../MoneroAddressViewModel.kt |
Key Compose patterns to follow:
- State holders:
remember { accountViewModel }pattern for ViewModels - Navigation: Use
NavBackStackEntryand typed routes - Material3:
Scaffold,TopAppBar,Button,TextField,IconButton,CircularProgressIndicator - Import path changes:
User→commons.model.User,Note→commons.model.Note - Compose Material3 imports:
androidx.compose.material3.*
Priority D: Build and Verify
Run the full build after completing wiring:
cd ~/projects/amethyst && ./gradlew assembleFdroidDebug
Then check for any remaining lint/compiler errors specifically in monero files.
Build Commands
Verify baseline builds
cd ~/projects/amethyst && ./gradlew assembleFdroidDebug | tail -3
Get the diff between Garnet and the fork point
cd ~/projects/garnet
git diff 99614f07..bfaf92e5 --stat # 103 files
git diff 99614f07..bfaf92e5 --name-status # file list with A/M/D status
Extract native libs from garnet releases
curl -L -o /tmp/garnet.apk "https://github.com/retrnull/garnet/releases/download/v0.3.0/app-mainnet-fdroid-universal-release.apk"
unzip -o /tmp/garnet.apk -d /tmp/garnet/
# libmonerujo.so is in extracted-libs/lib/<abi>/
Compile only Kotlin (fast check for monero-related errors)
cd ~/projects/amethyst && ./gradlew compileFdroidDebugKotlin 2>&1 | grep -E "^e:|FAILED|SUCCESS"
Count monero compilation errors
./gradlew compileFdroidDebugKotlin 2>&1 | grep "^e:" | grep -i monero | wc -l
Watch monero-specific errors in real time
./gradlew compileFdroidDebugKotlin 2>&1 | tee /tmp/build.log | grep "^e:" | grep -i monero
Common Error Patterns We Fixed
These are specific compile/lint errors encountered during the port, with fixes applied:
1. No such file or directory: 'com.vitorpamplona.quartz.events.*'
Cause: Quartz module restructured from main/java/com/vitorpamplona/quartz/events/ to commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/
Fix: Replace all old imports with new package paths. Mapping table above.
2. Cannot access 'com.vitorpamplona.quartz.nip01Core.enc.KeyPair'
Cause: KeyPair moved from enc.* to crypto.*
Fix: com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
3. Unresolved reference: NostrSignerInternal from signs.*
Cause: Signers package renamed from signs to signers
Fix: com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
4. Val cannot be reassigned / Property setter not found
Cause: Garnet used var properties; current codebase uses val with delegation
Fix: Use cache.update(...) pattern or MutableStateFlow.update { } instead of direct assignment.
5. Cannot inline class: KeyPair
Cause: KeyPair was a regular class in Garnet; now it may be a value class or internal class
Fix: Use KeyPair constructor properly or access through NostrSignerInternal factory.
6. Unresolved reference: applicationContext on Amethyst.instance
Cause: Amethyst renamed applicationContext to appContext
Fix: Amethyst.instance.appContext
7. Function 'justConsume' doesn't have the expected number of parameters
Cause: Garnet used 1-parameter justConsume(event), new version uses 3: justConsume(event, relay, isOwn)
Fix: Add relay and isOwn parameters: cache.justConsume(event, null, true) for own events.
8. Type mismatch: inferred type is commons.model.User but ...
Cause: User moved from amethyst.model.User to commons.model.User
Fix: Update all imports: com.vitorpamplona.amethyst.commons.model.User
9. No parameter with name 'autoCorrect' in TextField
Cause: Material3 TextField removed autoCorrect parameter in the new Compose BOM
Fix: Remove autoCorrect = false parameter from TextField calls.
10. Unresolved reference: rememberRipple
Cause: Material3 ripple API changed/removed
Fix: Use rememberRipple(color = ...) or remove the ripple parameter from composables.
11. Cannot find heading or HeadingStyle
Cause: HeadingStyle enum moved or was renamed during Compose upgrade
Fix: Check current richtext package for the correct heading/composable type.
12. No parameterless constructor found on sealed interfaces
Cause: Some sealed hierarchies moved from sealed class to sealed interface
Fix: Create instances via companion object create() methods or object expressions, not constructors.
Quick Commands for New Session
All paths use ~/ or $HOME — do NOT use absolute /home/ paths:
# Verify amethyst builds
cd ~/projects/amethyst && ./gradlew assembleFdroidDebug | tail -3
# Count remaining monero errors
./gradlew compileFdroidDebugKotlin 2>&1 | grep "^e:" -i monero | wc -l
# Get garnet diff for reference
cd ~/projects/garnet && git diff 99614f07..bfaf92e5 --stat
# Check current monero files in amethyst
find ~/projects/amethyst -name "*.kt" -path "*monero*" -not -path "*/build/*"
# Find all new files added this session
git status --short | grep "^A "
# Find all modified files
git status --short | grep "^M "