Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ require("flutter-tools").setup {
-- this will show the current version of the flutter app from the pubspec.yaml file
app_version = false,
-- set to true to be able use the 'flutter_tools_decorations.device' in your statusline
-- this will show the currently running device if an application was started with a specific
-- device
-- this will show the running device, or, while nothing is running in a Flutter project,
-- the device `flutter run` would pick by default
device = false,
-- set to true to be able use the 'flutter_tools_decorations.project_config' in your statusline
-- this will show the currently selected project configuration
Expand Down Expand Up @@ -413,7 +413,7 @@ dictionary that is created if you have set any of the decorations to `true` in y
The currently supported decorations are:

- App version - The current version of the app from the `pubspec.yaml`.
- Device - the running device, or, while nothing is running, the device `flutter run` would default to
- Device - the running device, or, while nothing is running in a Flutter project, the device `flutter run` would pick by default (nothing when it would prompt for one)
- Project config - the name of the currently selected project configuration

To add them to your config you can do something like
Expand Down
1 change: 1 addition & 0 deletions lua/flutter-tools/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,7 @@ if __TEST then
M.__run = run
M.__get_run_args = get_run_args
M.__update_device_from_output = update_device_from_output
M.__set_current_device = set_current_device
end

return M
4 changes: 3 additions & 1 deletion lua/flutter-tools/decorations.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ end
local function device_show()
local device = commands.current_device()
if device then return set_decoration_item("device", device) end
devices.get_default_device(function(default_device)
local project_root = vim.fs.root(0, "pubspec.yaml")
if not project_root then return set_decoration_item("device", nil) end
devices.get_default_device(project_root, function(default_device)
if commands.current_device() then return end
set_decoration_item("device", default_device)
end)
Expand Down
67 changes: 49 additions & 18 deletions lua/flutter-tools/devices.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,13 @@ local M = {
local EMULATOR = 1
local DEVICE = 2

local NON_EPHEMERAL_PLATFORMS = {
["darwin"] = true,
["darwin-arm64"] = true,
["darwin-x64"] = true,
["linux-arm64"] = true,
["linux-x64"] = true,
["windows-arm64"] = true,
["windows-x64"] = true,
["web-javascript"] = true,
local PLATFORM_DIRECTORIES = {
{ prefix = "android", directory = "android", ephemeral = true },
{ prefix = "ios", directory = "ios", ephemeral = true },
{ prefix = "darwin", directory = "macos", ephemeral = false },
{ prefix = "linux", directory = "linux", ephemeral = false },
{ prefix = "windows", directory = "windows", ephemeral = false },
{ prefix = "web", directory = "web", ephemeral = false },
}

---@param result string[]
Expand Down Expand Up @@ -109,34 +107,67 @@ local function to_device(entry)
}
end

---@param target_platform string?
local function platform_info(target_platform)
if not target_platform then return end
for _, info in ipairs(PLATFORM_DIRECTORIES) do
if vim.startswith(target_platform, info.prefix) then return info end
end
end

---@param entry table
---@param project_root string?
local function is_supported(entry, project_root)
if entry.isSupported == false then return false end
local info = platform_info(entry.targetPlatform)
if not project_root or not info then return true end
return vim.uv.fs_stat(vim.fs.joinpath(project_root, info.directory)) ~= nil
end

---Pick the device `flutter run` would use without an explicit `-d` argument.
---Mirrors flutter's own resolution: a single supported device wins, otherwise
---a single ephemeral (non desktop/web) device wins, otherwise flutter prompts.
---A device only counts as supported if the project has its platform directory.
---@param result string[]
---@param project_root string?
---@return Device?
function M.resolve_default_device(result)
local ok, decoded = pcall(vim.json.decode, table.concat(result, "\n"))
function M.resolve_default_device(result, project_root)
-- flutter can print notices such as the startup lock message before the JSON
local json_start
for index, line in ipairs(result) do
if vim.startswith(line, "[") then
json_start = index
break
end
end
if not json_start then return end
local json = table.concat(vim.list_slice(result, json_start), "\n")
local ok, decoded = pcall(vim.json.decode, json)
if not ok or type(decoded) ~= "table" then return end

local supported = vim.tbl_filter(function(entry) return entry.isSupported ~= false end, decoded)
local supported = vim.tbl_filter(
function(entry) return is_supported(entry, project_root) end,
decoded
)
if #supported == 0 then return end
if #supported == 1 then return to_device(supported[1]) end

local ephemeral = vim.tbl_filter(
function(entry) return not NON_EPHEMERAL_PLATFORMS[entry.targetPlatform] end,
supported
)
local ephemeral = vim.tbl_filter(function(entry)
local info = platform_info(entry.targetPlatform)
return not info or info.ephemeral
end, supported)
if #ephemeral == 1 then return to_device(ephemeral[1]) end
end

---Asynchronously get the device `flutter run` would default to
---@param project_root string
---@param callback fun(device: Device?)
function M.get_default_device(callback)
function M.get_default_device(project_root, callback)
executable.flutter(function(cmd)
local job = Job:new({ command = cmd, args = { "devices", "--machine" } })
job:after(vim.schedule_wrap(function(j, code)
if code ~= 0 then return callback(nil) end
callback(M.resolve_default_device(j:result()))
callback(M.resolve_default_device(j:result(), project_root))
end))
job:start()
end)
Expand Down
2 changes: 2 additions & 0 deletions tests/commands_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,11 @@ describe("commands", function()
end)

it("should keep the device id when the name is parsed from the run output", function()
commands.__set_current_device({ id = "macos" })
commands.__update_device_from_output("Launching lib/main.dart on macOS in debug mode...")

local device = commands.current_device()
assert.equal("macos", device.id)
assert.equal("macOS", device.name)
end)

Expand Down
39 changes: 39 additions & 0 deletions tests/devices_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,45 @@ INFO | Storing crashdata in: /tmp/android-ts/emu-crash-34.2.14.db, detection
assert.is_nil(device)
end)

it("should treat every desktop architecture as non-ephemeral", function()
local device = devices.resolve_default_device(machine_output({
{
name = "Pixel 8",
id = "emulator-5554",
isSupported = true,
targetPlatform = "android-arm64",
},
{ name = "Linux", id = "linux", isSupported = true, targetPlatform = "linux-riscv64" },
}))

assert.equal("emulator-5554", device.id)
end)

it("should skip devices whose platform directory is missing from the project", function()
local project_root = vim.fn.tempname()
vim.fn.mkdir(vim.fs.joinpath(project_root, "macos"), "p")

local device = devices.resolve_default_device(
machine_output({
{ name = "iPhone 16", id = "sim-id", isSupported = true, targetPlatform = "ios" },
{ name = "macOS", id = "macos", isSupported = true, targetPlatform = "darwin" },
}),
project_root
)
vim.fn.delete(project_root, "rf")

assert.equal("macos", device.id)
end)

it("should skip notices printed before the JSON output", function()
local output = machine_output({
{ name = "macOS", id = "macos", isSupported = true, targetPlatform = "darwin" },
})
table.insert(output, 1, "Waiting for another flutter command to release the startup lock...")

assert.equal("macos", devices.resolve_default_device(output).id)
end)

it("should return nil for no devices or invalid output", function()
assert.is_nil(devices.resolve_default_device(machine_output({})))
assert.is_nil(devices.resolve_default_device({ "not json" }))
Expand Down
Loading